mirror of
https://git.lapiole.org/dani/ansible-roles.git
synced 2025-04-12 00:03:17 +02:00
Update to 2024-07-22 23:00
This commit is contained in:
parent
f5421b17f0
commit
cd302033bd
@ -9,12 +9,20 @@ jitsi_user: jitsi
|
||||
jitsi_web_src_ip:
|
||||
- 0.0.0.0/0
|
||||
|
||||
jitsi_jicofo_git_url: https://github.com/jitsi/jicofo.git
|
||||
jitsi_jigasi_git_url: https://github.com/jitsi/jigasi.git
|
||||
jitsi_meet_git_url: https://github.com/jitsi/jitsi-meet.git
|
||||
jitsi_version: 9584
|
||||
|
||||
# Should ansible handle upgrades, or only initial install ?
|
||||
jitsi_manage_upgrade: true
|
||||
jitsi_jicofo_archive_url: https://github.com/jitsi/jicofo/archive/refs/tags/stable/jitsi-meet_{{ jitsi_version }}.tar.gz
|
||||
jitsi_jicofo_archive_sha256: 0be6e661a962e842704e8d2bdccfd28be21a97664883f458224e39a44679393f
|
||||
|
||||
# Jigasi has no release, nor tags, so use master
|
||||
jitsi_jigasi_archive_url: https://github.com/jitsi/jigasi/archive/refs/heads/master.tar.gz
|
||||
|
||||
jitsi_meet_archive_url: https://github.com/jitsi/jitsi-meet/archive/refs/tags/stable/jitsi-meet_{{ jitsi_version }}.tar.gz
|
||||
jitsi_meet_archive_sha256: fe52cd45159af7b9716043aae00a3f37e5874e31801261ec65d0ce9dc02c368f
|
||||
|
||||
jitsi_excalidraw_version: x21
|
||||
jitsi_excalidraw_archive_url: https://github.com/jitsi/excalidraw-backend/archive/refs/tags/{{ jitsi_excalidraw_version }}.tar.gz
|
||||
jitsi_excalidraw_archive_sha256: 7733c33667c7d9d022c1994c66819a57e44ca1c0dc71b3b71d9bcb10a09791f4
|
||||
|
||||
# XMPP server to connect to. Default is the same machine
|
||||
jitsi_xmpp_server: "{{ inventory_hostname }}"
|
||||
@ -149,10 +157,9 @@ jitsi_meet_conf_base:
|
||||
disabled: true
|
||||
giphy:
|
||||
enabled: true
|
||||
breakoutRooms:
|
||||
hideAddRoomButton: false
|
||||
hideAutoAssignButton: true
|
||||
hideJoinRoomButton: false
|
||||
whiteboard:
|
||||
enabled: true
|
||||
collabServerBaseUrl: 'https://{{ jitsi_domain }}/'
|
||||
|
||||
jitsi_meet_conf_extra: {}
|
||||
jitsi_meet_conf: "{{ jitsi_meet_conf_base | combine(jitsi_meet_conf_extra, recursive=true) }}"
|
||||
|
259
roles/jitsi/files/prosody/modules/luajwtjitsi.lib.lua
Normal file
259
roles/jitsi/files/prosody/modules/luajwtjitsi.lib.lua
Normal file
@ -0,0 +1,259 @@
|
||||
local cjson_safe = require 'cjson.safe'
|
||||
local basexx = require 'basexx'
|
||||
local digest = require 'openssl.digest'
|
||||
local hmac = require 'openssl.hmac'
|
||||
local pkey = require 'openssl.pkey'
|
||||
|
||||
-- Generates an RSA signature of the data.
|
||||
-- @param data The data to be signed.
|
||||
-- @param key The private signing key in PEM format.
|
||||
-- @param algo The digest algorithm to user when generating the signature: sha256, sha384, or sha512.
|
||||
-- @return The signature or nil and an error message.
|
||||
local function signRS (data, key, algo)
|
||||
local privkey = pkey.new(key)
|
||||
if privkey == nil then
|
||||
return nil, 'Not a private PEM key'
|
||||
else
|
||||
local datadigest = digest.new(algo):update(data)
|
||||
return privkey:sign(datadigest)
|
||||
end
|
||||
end
|
||||
|
||||
-- Verifies an RSA signature on the data.
|
||||
-- @param data The signed data.
|
||||
-- @param signature The signature to be verified.
|
||||
-- @param key The public key of the signer.
|
||||
-- @param algo The digest algorithm to user when generating the signature: sha256, sha384, or sha512.
|
||||
-- @return True if the signature is valid, false otherwise. Also returns false if the key is invalid.
|
||||
local function verifyRS (data, signature, key, algo)
|
||||
local pubkey = pkey.new(key)
|
||||
if pubkey == nil then
|
||||
return false
|
||||
end
|
||||
|
||||
local datadigest = digest.new(algo):update(data)
|
||||
return pubkey:verify(signature, datadigest)
|
||||
end
|
||||
|
||||
local alg_sign = {
|
||||
['HS256'] = function(data, key) return hmac.new(key, 'sha256'):final(data) end,
|
||||
['HS384'] = function(data, key) return hmac.new(key, 'sha384'):final(data) end,
|
||||
['HS512'] = function(data, key) return hmac.new(key, 'sha512'):final(data) end,
|
||||
['RS256'] = function(data, key) return signRS(data, key, 'sha256') end,
|
||||
['RS384'] = function(data, key) return signRS(data, key, 'sha384') end,
|
||||
['RS512'] = function(data, key) return signRS(data, key, 'sha512') end
|
||||
}
|
||||
|
||||
local alg_verify = {
|
||||
['HS256'] = function(data, signature, key) return signature == alg_sign['HS256'](data, key) end,
|
||||
['HS384'] = function(data, signature, key) return signature == alg_sign['HS384'](data, key) end,
|
||||
['HS512'] = function(data, signature, key) return signature == alg_sign['HS512'](data, key) end,
|
||||
['RS256'] = function(data, signature, key) return verifyRS(data, signature, key, 'sha256') end,
|
||||
['RS384'] = function(data, signature, key) return verifyRS(data, signature, key, 'sha384') end,
|
||||
['RS512'] = function(data, signature, key) return verifyRS(data, signature, key, 'sha512') end
|
||||
}
|
||||
|
||||
-- Splits a token into segments, separated by '.'.
|
||||
-- @param token The full token to be split.
|
||||
-- @return A table of segments.
|
||||
local function split_token(token)
|
||||
local segments={}
|
||||
for str in string.gmatch(token, "([^\\.]+)") do
|
||||
table.insert(segments, str)
|
||||
end
|
||||
return segments
|
||||
end
|
||||
|
||||
-- Parses a JWT token into it's header, body, and signature.
|
||||
-- @param token The JWT token to be parsed.
|
||||
-- @return A JSON header and body represented as a table, and a signature.
|
||||
local function parse_token(token)
|
||||
local segments=split_token(token)
|
||||
if #segments ~= 3 then
|
||||
return nil, nil, nil, "Invalid token"
|
||||
end
|
||||
|
||||
local header, err = cjson_safe.decode(basexx.from_url64(segments[1]))
|
||||
if err then
|
||||
return nil, nil, nil, "Invalid header"
|
||||
end
|
||||
|
||||
local body, err = cjson_safe.decode(basexx.from_url64(segments[2]))
|
||||
if err then
|
||||
return nil, nil, nil, "Invalid body"
|
||||
end
|
||||
|
||||
local sig, err = basexx.from_url64(segments[3])
|
||||
if err then
|
||||
return nil, nil, nil, "Invalid signature"
|
||||
end
|
||||
|
||||
return header, body, sig
|
||||
end
|
||||
|
||||
-- Removes the signature from a JWT token.
|
||||
-- @param token A JWT token.
|
||||
-- @return The token without its signature.
|
||||
local function strip_signature(token)
|
||||
local segments=split_token(token)
|
||||
if #segments ~= 3 then
|
||||
return nil, nil, nil, "Invalid token"
|
||||
end
|
||||
|
||||
table.remove(segments)
|
||||
return table.concat(segments, ".")
|
||||
end
|
||||
|
||||
-- Verifies that a claim is in a list of allowed claims. Allowed claims can be exact values, or the
|
||||
-- catch all wildcard '*'.
|
||||
-- @param claim The claim to be verified.
|
||||
-- @param acceptedClaims A table of accepted claims.
|
||||
-- @return True if the claim was allowed, false otherwise.
|
||||
local function verify_claim(claim, acceptedClaims)
|
||||
for i, accepted in ipairs(acceptedClaims) do
|
||||
if accepted == '*' then
|
||||
return true;
|
||||
end
|
||||
if claim == accepted then
|
||||
return true;
|
||||
end
|
||||
end
|
||||
|
||||
return false;
|
||||
end
|
||||
|
||||
local M = {}
|
||||
|
||||
-- Encodes the data into a signed JWT token.
|
||||
-- @param data The data the put in the body of the JWT token.
|
||||
-- @param key The key to use for signing the JWT token.
|
||||
-- @param alg The signature algorithm to use: HS256, HS384, HS512, RS256, RS384, or RS512.
|
||||
-- @param header Additional values to put in the JWT header.
|
||||
-- @param The resulting JWT token, or nil and an error message.
|
||||
function M.encode(data, key, alg, header)
|
||||
if type(data) ~= 'table' then return nil, "Argument #1 must be table" end
|
||||
if type(key) ~= 'string' then return nil, "Argument #2 must be string" end
|
||||
|
||||
alg = alg or "HS256"
|
||||
|
||||
if not alg_sign[alg] then
|
||||
return nil, "Algorithm not supported"
|
||||
end
|
||||
|
||||
header = header or {}
|
||||
|
||||
header['typ'] = 'JWT'
|
||||
header['alg'] = alg
|
||||
|
||||
local headerEncoded, err = cjson_safe.encode(header)
|
||||
if headerEncoded == nil then
|
||||
return nil, err
|
||||
end
|
||||
|
||||
local dataEncoded, err = cjson_safe.encode(data)
|
||||
if dataEncoded == nil then
|
||||
return nil, err
|
||||
end
|
||||
|
||||
local segments = {
|
||||
basexx.to_url64(headerEncoded),
|
||||
basexx.to_url64(dataEncoded)
|
||||
}
|
||||
|
||||
local signing_input = table.concat(segments, ".")
|
||||
local signature, error = alg_sign[alg](signing_input, key)
|
||||
if signature == nil then
|
||||
return nil, error
|
||||
end
|
||||
|
||||
segments[#segments+1] = basexx.to_url64(signature)
|
||||
|
||||
return table.concat(segments, ".")
|
||||
end
|
||||
|
||||
-- Verify that the token is valid, and if it is return the decoded JSON payload data.
|
||||
-- @param token The token to verify.
|
||||
-- @param expectedAlgo The signature algorithm the caller expects the token to be signed with:
|
||||
-- HS256, HS384, HS512, RS256, RS384, or RS512.
|
||||
-- @param key The verification key used for the signature.
|
||||
-- @param acceptedIssuers Optional table of accepted issuers. If not nil, the 'iss' claim will be
|
||||
-- checked against this list.
|
||||
-- @param acceptedAudiences Optional table of accepted audiences. If not nil, the 'aud' claim will
|
||||
-- be checked against this list.
|
||||
-- @return A table representing the JSON body of the token, or nil and an error message.
|
||||
function M.verify(token, expectedAlgo, key, acceptedIssuers, acceptedAudiences)
|
||||
if type(token) ~= 'string' then return nil, "token argument must be string" end
|
||||
if type(expectedAlgo) ~= 'string' then return nil, "algorithm argument must be string" end
|
||||
if type(key) ~= 'string' then return nil, "key argument must be string" end
|
||||
if acceptedIssuers ~= nil and type(acceptedIssuers) ~= 'table' then
|
||||
return nil, "acceptedIssuers argument must be table"
|
||||
end
|
||||
if acceptedAudiences ~= nil and type(acceptedAudiences) ~= 'table' then
|
||||
return nil, "acceptedAudiences argument must be table"
|
||||
end
|
||||
|
||||
if not alg_verify[expectedAlgo] then
|
||||
return nil, "Algorithm not supported"
|
||||
end
|
||||
|
||||
local header, body, sig, err = parse_token(token)
|
||||
if err ~= nil then
|
||||
return nil, err
|
||||
end
|
||||
|
||||
-- Validate header
|
||||
if not header.typ or header.typ ~= "JWT" then
|
||||
return nil, "Invalid typ"
|
||||
end
|
||||
|
||||
if not header.alg or header.alg ~= expectedAlgo then
|
||||
return nil, "Invalid or incorrect alg"
|
||||
end
|
||||
|
||||
-- Validate signature
|
||||
if not alg_verify[expectedAlgo](strip_signature(token), sig, key) then
|
||||
return nil, 'Invalid signature'
|
||||
end
|
||||
|
||||
-- Validate body
|
||||
if body.exp and type(body.exp) ~= "number" then
|
||||
return nil, "exp must be number"
|
||||
end
|
||||
|
||||
if body.nbf and type(body.nbf) ~= "number" then
|
||||
return nil, "nbf must be number"
|
||||
end
|
||||
|
||||
|
||||
if body.exp and os.time() >= body.exp then
|
||||
return nil, "Not acceptable by exp ("..tostring(os.time()-body.exp)..")"
|
||||
end
|
||||
|
||||
if body.nbf and os.time() < body.nbf then
|
||||
return nil, "Not acceptable by nbf"
|
||||
end
|
||||
|
||||
if acceptedIssuers ~= nil then
|
||||
local issClaim = body.iss;
|
||||
if issClaim == nil then
|
||||
return nil, "'iss' claim is missing";
|
||||
end
|
||||
if not verify_claim(issClaim, acceptedIssuers) then
|
||||
return nil, "invalid 'iss' claim";
|
||||
end
|
||||
end
|
||||
|
||||
if acceptedAudiences ~= nil then
|
||||
local audClaim = body.aud;
|
||||
if audClaim == nil then
|
||||
return nil, "'aud' claim is missing";
|
||||
end
|
||||
if not verify_claim(audClaim, acceptedAudiences) then
|
||||
return nil, "invalid 'aud' claim";
|
||||
end
|
||||
end
|
||||
|
||||
return body
|
||||
end
|
||||
|
||||
return M
|
@ -0,0 +1,78 @@
|
||||
-- Anonymous authentication with extras:
|
||||
-- * session resumption
|
||||
-- Copyright (C) 2021-present 8x8, Inc.
|
||||
|
||||
local generate_uuid = require "util.uuid".generate;
|
||||
local new_sasl = require "util.sasl".new;
|
||||
local sasl = require "util.sasl";
|
||||
local sessions = prosody.full_sessions;
|
||||
|
||||
-- define auth provider
|
||||
local provider = {};
|
||||
|
||||
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)
|
||||
-- Custom session matching so we can resume session even with randomly
|
||||
-- generated user IDs.
|
||||
local function get_username(self, message)
|
||||
if (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
|
||||
|
||||
return true;
|
||||
end
|
||||
|
||||
return new_sasl(module.host, { anonymous = get_username });
|
||||
end
|
||||
|
||||
module:provides("auth", provider);
|
||||
|
||||
local function anonymous(self, message)
|
||||
-- Same as the vanilla anonymous auth plugin
|
||||
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
|
||||
-- Session was not resumed
|
||||
self.username = username;
|
||||
end
|
||||
return "success";
|
||||
else
|
||||
return "failure", err, msg;
|
||||
end
|
||||
end
|
||||
|
||||
sasl.registerMechanism("ANONYMOUS", {"anonymous"}, anonymous);
|
@ -0,0 +1,65 @@
|
||||
-- Authentication with shared secret where the username is ignored
|
||||
-- Copyright (C) 2023-present 8x8, Inc.
|
||||
|
||||
local new_sasl = require "util.sasl".new;
|
||||
local saslprep = require "util.encodings".stringprep.saslprep;
|
||||
local secure_equals = require "util.hashes".equals;
|
||||
|
||||
local shared_secret = module:get_option_string('shared_secret');
|
||||
local shared_secret_prev = module:get_option_string('shared_secret_prev');
|
||||
if shared_secret == nil then
|
||||
module:log('error', 'No shared_secret specified. No secret to operate on!');
|
||||
return;
|
||||
end
|
||||
|
||||
module:depends("jitsi_session");
|
||||
|
||||
-- define auth provider
|
||||
local provider = {};
|
||||
|
||||
function provider.test_password(username, password)
|
||||
password = saslprep(password);
|
||||
if not password then
|
||||
return nil, "Password fails SASLprep.";
|
||||
end
|
||||
|
||||
if secure_equals(password, saslprep(shared_secret)) then
|
||||
return true;
|
||||
elseif (shared_secret_prev ~= nil and secure_equals(password, saslprep(shared_secret_prev))) then
|
||||
module:log("info", "Accepting login using previous shared secret, username=%s", username);
|
||||
return true;
|
||||
else
|
||||
return nil, "Auth failed. Invalid username or password.";
|
||||
end
|
||||
end
|
||||
|
||||
function provider.get_password(username)
|
||||
return shared_secret;
|
||||
end
|
||||
|
||||
function provider.set_password(username, password)
|
||||
return nil, "Set password not supported";
|
||||
end
|
||||
|
||||
function provider.user_exists(username)
|
||||
return true; -- all usernames exist
|
||||
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 getpass_authentication_profile = {
|
||||
plain = function(_, username, realm)
|
||||
return shared_secret, true;
|
||||
end
|
||||
};
|
||||
return new_sasl(module.host, getpass_authentication_profile);
|
||||
end
|
||||
|
||||
module:provides("auth", provider);
|
125
roles/jitsi/files/prosody/modules/mod_auth_ldap.lua
Normal file
125
roles/jitsi/files/prosody/modules/mod_auth_ldap.lua
Normal file
@ -0,0 +1,125 @@
|
||||
-- mod_auth_ldap
|
||||
|
||||
local new_sasl = require "util.sasl".new;
|
||||
local lualdap = require "lualdap";
|
||||
local function ldap_filter_escape(s) return (s:gsub("[*()\\%z]", function(c) return ("\\%02x"):format(c:byte()) end)); end
|
||||
|
||||
-- Config options
|
||||
local ldap_server = module:get_option_string("ldap_server", "localhost");
|
||||
local ldap_rootdn = module:get_option_string("ldap_rootdn", "");
|
||||
local ldap_password = module:get_option_string("ldap_password", "");
|
||||
local ldap_tls = module:get_option_boolean("ldap_tls");
|
||||
local ldap_scope = module:get_option_string("ldap_scope", "onelevel");
|
||||
local ldap_filter = module:get_option_string("ldap_filter", "(uid=$user)"):gsub("%%s", "$user", 1);
|
||||
local ldap_base = assert(module:get_option_string("ldap_base"), "ldap_base is a required option for ldap");
|
||||
local ldap_mode = module:get_option_string("ldap_mode", "bind");
|
||||
local host = ldap_filter_escape(module:get_option_string("realm", module.host));
|
||||
|
||||
-- Initiate connection
|
||||
local ld = nil;
|
||||
module.unload = function() if ld then pcall(ld, ld.close); end end
|
||||
|
||||
function ldap_do_once(method, ...)
|
||||
if ld == nil then
|
||||
local err;
|
||||
ld, err = lualdap.open_simple(ldap_server, ldap_rootdn, ldap_password, ldap_tls);
|
||||
if not ld then return nil, err, "reconnect"; end
|
||||
end
|
||||
|
||||
local success, iterator, invariant, initial = pcall(ld[method], ld, ...);
|
||||
if not success then ld = nil; return nil, iterator, "search"; end
|
||||
|
||||
local success, dn, attr = pcall(iterator, invariant, initial);
|
||||
if not success then ld = nil; return success, dn, "iter"; end
|
||||
|
||||
return dn, attr, "return";
|
||||
end
|
||||
|
||||
function ldap_do(method, retry_count, ...)
|
||||
local dn, attr, where;
|
||||
for i=1,1+retry_count do
|
||||
dn, attr, where = ldap_do_once(method, ...);
|
||||
if dn or not(attr) then break; end -- nothing or something found
|
||||
module:log("warn", "LDAP: %s %s (in %s)", tostring(dn), tostring(attr), where);
|
||||
-- otherwise retry
|
||||
end
|
||||
if not dn and attr then
|
||||
module:log("error", "LDAP: %s", tostring(attr));
|
||||
end
|
||||
return dn, attr;
|
||||
end
|
||||
|
||||
local function get_user(username)
|
||||
module:log("debug", "get_user(%q)", username);
|
||||
return ldap_do("search", 2, {
|
||||
base = ldap_base;
|
||||
scope = ldap_scope;
|
||||
sizelimit = 1;
|
||||
filter = ldap_filter:gsub("%$(%a+)", {
|
||||
user = ldap_filter_escape(username);
|
||||
host = host;
|
||||
});
|
||||
});
|
||||
end
|
||||
|
||||
local provider = {};
|
||||
|
||||
function provider.create_user(username, password)
|
||||
return nil, "Account creation not available with LDAP.";
|
||||
end
|
||||
|
||||
function provider.user_exists(username)
|
||||
return not not get_user(username);
|
||||
end
|
||||
|
||||
function provider.set_password(username, password)
|
||||
local dn, attr = get_user(username);
|
||||
if not dn then return nil, attr end
|
||||
if attr.userPassword == password then return true end
|
||||
return ldap_do("modify", 2, dn, { '=', userPassword = password });
|
||||
end
|
||||
|
||||
if ldap_mode == "getpasswd" then
|
||||
function provider.get_password(username)
|
||||
local dn, attr = get_user(username);
|
||||
if dn and attr then
|
||||
return attr.userPassword;
|
||||
end
|
||||
end
|
||||
|
||||
function provider.test_password(username, password)
|
||||
return provider.get_password(username) == password;
|
||||
end
|
||||
|
||||
function provider.get_sasl_handler()
|
||||
return new_sasl(module.host, {
|
||||
plain = function(sasl, username)
|
||||
local password = provider.get_password(username);
|
||||
if not password then return "", nil; end
|
||||
return password, true;
|
||||
end
|
||||
});
|
||||
end
|
||||
elseif ldap_mode == "bind" then
|
||||
local function test_password(userdn, password)
|
||||
return not not lualdap.open_simple(ldap_server, userdn, password, ldap_tls);
|
||||
end
|
||||
|
||||
function provider.test_password(username, password)
|
||||
local dn = get_user(username);
|
||||
if not dn then return end
|
||||
return test_password(dn, password)
|
||||
end
|
||||
|
||||
function provider.get_sasl_handler()
|
||||
return new_sasl(module.host, {
|
||||
plain_test = function(sasl, username, password)
|
||||
return provider.test_password(username, password), true;
|
||||
end
|
||||
});
|
||||
end
|
||||
else
|
||||
module:log("error", "Unsupported ldap_mode %s", tostring(ldap_mode));
|
||||
end
|
||||
|
||||
module:provides("auth", provider);
|
167
roles/jitsi/files/prosody/modules/mod_auth_token.lua
Normal file
167
roles/jitsi/files/prosody/modules/mod_auth_token.lua
Normal file
@ -0,0 +1,167 @@
|
||||
-- Token authentication
|
||||
-- Copyright (C) 2021-present 8x8, Inc.
|
||||
|
||||
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
|
||||
|
||||
module:depends("jitsi_session");
|
||||
|
||||
local measure_pre_fetch_fail = module:measure('pre_fetch_fail', 'counter');
|
||||
local measure_verify_fail = module:measure('verify_fail', 'counter');
|
||||
local measure_success = module:measure('success', 'counter');
|
||||
local measure_ban = module:measure('ban', 'counter');
|
||||
local measure_post_auth_fail = module:measure('post_auth_fail', 'counter');
|
||||
|
||||
-- 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;
|
||||
|
||||
local token = nil;
|
||||
|
||||
-- extract token from Authorization header
|
||||
if request.headers["authorization"] then
|
||||
-- assumes the header value starts with "Bearer "
|
||||
token = request.headers["authorization"]:sub(8,#request.headers["authorization"])
|
||||
end
|
||||
|
||||
-- allow override of token via query parameter
|
||||
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 being 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
|
||||
|
||||
if query and params.token then
|
||||
token = params.token;
|
||||
end
|
||||
end
|
||||
|
||||
-- in either case set auth_token in the session
|
||||
session.auth_token = token;
|
||||
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
|
||||
module:log("warn",
|
||||
"Error verifying token on pre authentication stage:%s, reason:%s", pre_event_result.error, pre_event_result.reason);
|
||||
session.auth_token = nil;
|
||||
measure_pre_fetch_fail(1);
|
||||
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
|
||||
module:log("warn",
|
||||
"Error verifying token err:%s, reason:%s tenant:%s room:%s",
|
||||
error, reason, session.jitsi_web_query_prefix, session.jitsi_web_query_room);
|
||||
session.auth_token = nil;
|
||||
measure_verify_fail(1);
|
||||
return res, error, reason;
|
||||
end
|
||||
|
||||
local shouldAllow = prosody.events.fire_event("jitsi-access-ban-check", session);
|
||||
if shouldAllow == false then
|
||||
module:log("warn", "user is banned")
|
||||
measure_ban(1);
|
||||
return false, "not-allowed", "user is banned";
|
||||
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
|
||||
module:log("warn",
|
||||
"Error verifying token on post authentication stage :%s, reason:%s", post_event_result.error, post_event_result.reason);
|
||||
session.auth_token = nil;
|
||||
measure_post_auth_fail(1);
|
||||
return post_event_result.res, post_event_result.error, post_event_result.reason;
|
||||
end
|
||||
|
||||
measure_success(1);
|
||||
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);
|
6
roles/jitsi/files/prosody/modules/mod_av_moderation.lua
Normal file
6
roles/jitsi/files/prosody/modules/mod_av_moderation.lua
Normal file
@ -0,0 +1,6 @@
|
||||
local avmoderation_component = module:get_option_string('av_moderation_component', 'avmoderation.'..module.host);
|
||||
|
||||
-- Advertise AV Moderation so client can pick up the address and use it
|
||||
module:add_identity('component', 'av_moderation', avmoderation_component);
|
||||
|
||||
module:depends("jitsi_session");
|
@ -0,0 +1,331 @@
|
||||
local util = module:require 'util';
|
||||
local get_room_by_name_and_subdomain = util.get_room_by_name_and_subdomain;
|
||||
local is_healthcheck_room = util.is_healthcheck_room;
|
||||
local internal_room_jid_match_rewrite = util.internal_room_jid_match_rewrite;
|
||||
local room_jid_match_rewrite = util.room_jid_match_rewrite;
|
||||
local process_host_module = util.process_host_module;
|
||||
local array = require "util.array";
|
||||
local json = require 'cjson.safe';
|
||||
local st = require 'util.stanza';
|
||||
|
||||
local muc_component_host = module:get_option_string('muc_component');
|
||||
if muc_component_host == nil then
|
||||
module:log('error', 'No muc_component specified. No muc to operate on!');
|
||||
return;
|
||||
end
|
||||
|
||||
module:log('info', 'Starting av_moderation for %s', muc_component_host);
|
||||
|
||||
-- Returns the index of the given element in the table
|
||||
-- @param table in which to look
|
||||
-- @param elem the element for which to find the index
|
||||
function get_index_in_table(table, elem)
|
||||
for index, value in pairs(table) do
|
||||
if value == elem then
|
||||
return index
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Sends a json-message to the destination jid
|
||||
-- @param to_jid the destination jid
|
||||
-- @param json_message the message content to send
|
||||
function send_json_message(to_jid, json_message)
|
||||
local stanza = st.message({ from = module.host; to = to_jid; })
|
||||
:tag('json-message', { xmlns = 'http://jitsi.org/jitmeet' }):text(json_message):up();
|
||||
module:send(stanza);
|
||||
end
|
||||
|
||||
-- Notifies that av moderation has been enabled or disabled
|
||||
-- @param jid the jid to notify, if missing will notify all occupants
|
||||
-- @param enable whether it is enabled or disabled
|
||||
-- @param room the room
|
||||
-- @param actorJid the jid that is performing the enable/disable operation (the muc jid)
|
||||
-- @param mediaType the media type for the moderation
|
||||
function notify_occupants_enable(jid, enable, room, actorJid, mediaType)
|
||||
local body_json = {};
|
||||
body_json.type = 'av_moderation';
|
||||
body_json.enabled = enable;
|
||||
body_json.room = internal_room_jid_match_rewrite(room.jid);
|
||||
body_json.actor = actorJid;
|
||||
body_json.mediaType = mediaType;
|
||||
local body_json_str, error = json.encode(body_json);
|
||||
|
||||
if not body_json_str then
|
||||
module:log('error', 'error encoding json room:%s error:%s', room.jid, error);
|
||||
return;
|
||||
end
|
||||
|
||||
if jid then
|
||||
send_json_message(jid, body_json_str)
|
||||
else
|
||||
for _, occupant in room:each_occupant() do
|
||||
send_json_message(occupant.jid, body_json_str)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Notifies about a change to the whitelist. Notifies all moderators and admin and the jid itself
|
||||
-- @param jid the jid to notify about the change
|
||||
-- @param moderators whether to notify all moderators in the room
|
||||
-- @param room the room where to send it
|
||||
-- @param mediaType used only when a participant is approved (not sent to moderators)
|
||||
-- @param removed whether the jid is removed or added
|
||||
function notify_whitelist_change(jid, moderators, room, mediaType, removed)
|
||||
local body_json = {};
|
||||
body_json.type = 'av_moderation';
|
||||
body_json.room = internal_room_jid_match_rewrite(room.jid);
|
||||
body_json.whitelists = room.av_moderation;
|
||||
if removed then
|
||||
body_json.removed = true;
|
||||
end
|
||||
body_json.mediaType = mediaType;
|
||||
local moderators_body_json_str, error = json.encode(body_json);
|
||||
|
||||
if not moderators_body_json_str then
|
||||
module:log('error', 'error encoding moderator json room:%s error:%s', room.jid, error);
|
||||
return;
|
||||
end
|
||||
|
||||
body_json.whitelists = nil;
|
||||
if not removed then
|
||||
body_json.approved = true; -- we want to send to participants only that they were approved to unmute
|
||||
end
|
||||
local participant_body_json_str, error = json.encode(body_json);
|
||||
|
||||
if not participant_body_json_str then
|
||||
module:log('error', 'error encoding participant json room:%s error:%s', room.jid, error);
|
||||
return;
|
||||
end
|
||||
|
||||
for _, occupant in room:each_occupant() do
|
||||
if moderators and occupant.role == 'moderator' then
|
||||
send_json_message(occupant.jid, moderators_body_json_str);
|
||||
elseif occupant.jid == jid then
|
||||
-- if the occupant is not moderator we send him that it is approved
|
||||
-- if it is moderator we update him with the list, this is moderator joining or grant moderation was executed
|
||||
if occupant.role == 'moderator' then
|
||||
send_json_message(occupant.jid, moderators_body_json_str);
|
||||
else
|
||||
send_json_message(occupant.jid, participant_body_json_str);
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Notifies jid that is approved. This is a moderator to jid message to ask to unmute,
|
||||
-- @param jid the jid to notify about the change
|
||||
-- @param from the jid that triggered this
|
||||
-- @param room the room where to send it
|
||||
-- @param mediaType the mediaType it was approved for
|
||||
function notify_jid_approved(jid, from, room, mediaType)
|
||||
local body_json = {};
|
||||
body_json.type = 'av_moderation';
|
||||
body_json.room = internal_room_jid_match_rewrite(room.jid);
|
||||
body_json.approved = true; -- we want to send to participants only that they were approved to unmute
|
||||
body_json.mediaType = mediaType;
|
||||
body_json.from = from;
|
||||
|
||||
local json_message, error = json.encode(body_json);
|
||||
if not json_message then
|
||||
module:log('error', 'skip sending json message to:%s error:%s', jid, error);
|
||||
return;
|
||||
end
|
||||
|
||||
send_json_message(jid, json_message);
|
||||
end
|
||||
|
||||
-- receives messages from clients to the component sending A/V moderation enable/disable commands or adding
|
||||
-- jids to the whitelist
|
||||
function on_message(event)
|
||||
local session = event.origin;
|
||||
|
||||
-- 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 not session or not session.jitsi_web_query_room then
|
||||
return false;
|
||||
end
|
||||
|
||||
local moderation_command = event.stanza:get_child('av_moderation');
|
||||
|
||||
if moderation_command then
|
||||
-- get room name with tenant and find room
|
||||
local room = get_room_by_name_and_subdomain(session.jitsi_web_query_room, session.jitsi_web_query_prefix);
|
||||
|
||||
if not room then
|
||||
module:log('warn', 'No room found found for %s/%s',
|
||||
session.jitsi_web_query_prefix, session.jitsi_web_query_room);
|
||||
return false;
|
||||
end
|
||||
|
||||
-- check that the participant requesting is a moderator and is an occupant in the room
|
||||
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, room.jid);
|
||||
return false;
|
||||
end
|
||||
if occupant.role ~= 'moderator' then
|
||||
module:log('warn', 'Occupant %s is not moderator and not allowed this operation for %s', from, room.jid);
|
||||
return false;
|
||||
end
|
||||
|
||||
local mediaType = moderation_command.attr.mediaType;
|
||||
if mediaType then
|
||||
if mediaType ~= 'audio' and mediaType ~= 'video' then
|
||||
module:log('warn', 'Wrong mediaType %s for %s', mediaType, room.jid);
|
||||
return false;
|
||||
end
|
||||
else
|
||||
module:log('warn', 'Missing mediaType for %s', room.jid);
|
||||
return false;
|
||||
end
|
||||
|
||||
if moderation_command.attr.enable ~= nil then
|
||||
local enabled;
|
||||
if moderation_command.attr.enable == 'true' then
|
||||
enabled = true;
|
||||
if room.av_moderation and room.av_moderation[mediaType] then
|
||||
module:log('warn', 'Concurrent moderator enable/disable request or something is out of sync');
|
||||
return true;
|
||||
else
|
||||
if not room.av_moderation then
|
||||
room.av_moderation = {};
|
||||
room.av_moderation_actors = {};
|
||||
end
|
||||
room.av_moderation[mediaType] = array{};
|
||||
room.av_moderation_actors[mediaType] = occupant.nick;
|
||||
end
|
||||
else
|
||||
enabled = false;
|
||||
if not room.av_moderation then
|
||||
module:log('warn', 'Concurrent moderator enable/disable request or something is out of sync');
|
||||
return true;
|
||||
else
|
||||
room.av_moderation[mediaType] = nil;
|
||||
room.av_moderation_actors[mediaType] = nil;
|
||||
|
||||
-- clears room.av_moderation if empty
|
||||
local is_empty = true;
|
||||
for key,_ in pairs(room.av_moderation) do
|
||||
if room.av_moderation[key] then
|
||||
is_empty = false;
|
||||
end
|
||||
end
|
||||
if is_empty then
|
||||
room.av_moderation = nil;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- send message to all occupants
|
||||
notify_occupants_enable(nil, enabled, room, occupant.nick, mediaType);
|
||||
return true;
|
||||
elseif moderation_command.attr.jidToWhitelist then
|
||||
local occupant_jid = moderation_command.attr.jidToWhitelist;
|
||||
-- check if jid is in the room, if so add it to whitelist
|
||||
-- inform all moderators and admins and the jid
|
||||
local occupant_to_add = room:get_occupant_by_nick(room_jid_match_rewrite(occupant_jid));
|
||||
if not occupant_to_add then
|
||||
module:log('warn', 'No occupant %s found for %s', occupant_jid, room.jid);
|
||||
return false;
|
||||
end
|
||||
|
||||
if room.av_moderation then
|
||||
local whitelist = room.av_moderation[mediaType];
|
||||
if not whitelist then
|
||||
whitelist = array{};
|
||||
room.av_moderation[mediaType] = whitelist;
|
||||
end
|
||||
whitelist:push(occupant_jid);
|
||||
|
||||
notify_whitelist_change(occupant_to_add.jid, true, room, mediaType, false);
|
||||
|
||||
return true;
|
||||
else
|
||||
-- this is a moderator asking the jid to unmute without enabling av moderation
|
||||
-- let's just send the event
|
||||
notify_jid_approved(occupant_to_add.jid, occupant.nick, room, mediaType);
|
||||
end
|
||||
elseif moderation_command.attr.jidToBlacklist then
|
||||
local occupant_jid = moderation_command.attr.jidToBlacklist;
|
||||
-- check if jid is in the room, if so remove it from the whitelist
|
||||
-- inform all moderators and admins
|
||||
local occupant_to_remove = room:get_occupant_by_nick(room_jid_match_rewrite(occupant_jid));
|
||||
if not occupant_to_remove then
|
||||
module:log('warn', 'No occupant %s found for %s', occupant_jid, room.jid);
|
||||
return false;
|
||||
end
|
||||
|
||||
if room.av_moderation then
|
||||
local whitelist = room.av_moderation[mediaType];
|
||||
if whitelist then
|
||||
local index = get_index_in_table(whitelist, occupant_jid)
|
||||
if(index) then
|
||||
whitelist:pop(index);
|
||||
notify_whitelist_change(occupant_to_remove.jid, true, room, mediaType, true);
|
||||
end
|
||||
end
|
||||
|
||||
return true;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- return error
|
||||
return false
|
||||
end
|
||||
|
||||
-- handles new occupants to inform them about the state enabled/disabled, new moderators also get and the whitelist
|
||||
function occupant_joined(event)
|
||||
local room, occupant = event.room, event.occupant;
|
||||
|
||||
if is_healthcheck_room(room.jid) then
|
||||
return;
|
||||
end
|
||||
|
||||
if room.av_moderation then
|
||||
for _,mediaType in pairs({'audio', 'video'}) do
|
||||
if room.av_moderation[mediaType] then
|
||||
notify_occupants_enable(
|
||||
occupant.jid, true, room, room.av_moderation_actors[mediaType], mediaType);
|
||||
end
|
||||
end
|
||||
|
||||
-- NOTE for some reason event.occupant.role is not reflecting the actual occupant role (when changed
|
||||
-- from allowners module) but iterating over room occupants returns the correct role
|
||||
for _, room_occupant in room:each_occupant() do
|
||||
-- if moderator send the whitelist
|
||||
if room_occupant.nick == occupant.nick and room_occupant.role == 'moderator' then
|
||||
notify_whitelist_change(room_occupant.jid, false, room);
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- when a occupant was granted moderator we need to update him with the whitelist
|
||||
function occupant_affiliation_changed(event)
|
||||
-- the actor can be nil if is coming from allowners or similar module we want to skip it here
|
||||
-- as we will handle it in occupant_joined
|
||||
if event.actor and event.affiliation == 'owner' and event.room.av_moderation then
|
||||
local room = event.room;
|
||||
-- event.jid is the bare jid of participant
|
||||
for _, occupant in room:each_occupant() do
|
||||
if occupant.bare_jid == event.jid then
|
||||
notify_whitelist_change(occupant.jid, false, room);
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- we will receive messages from the clients
|
||||
module:hook('message/host', on_message);
|
||||
|
||||
process_host_module(muc_component_host, function(host_module, host)
|
||||
module:log('info','Hook to muc events on %s', host);
|
||||
host_module:hook('muc-occupant-joined', occupant_joined, -2); -- make sure it runs after allowners or similar
|
||||
host_module:hook('muc-set-affiliation', occupant_affiliation_changed, -1);
|
||||
end);
|
@ -0,0 +1,23 @@
|
||||
-- global module
|
||||
-- validates certificates for all hosts used for s2soutinjection or s2sout_override
|
||||
module:set_global();
|
||||
|
||||
local s2s_overrides = module:get_option("s2s_connect_overrides");
|
||||
|
||||
if not s2s_overrides then
|
||||
s2s_overrides = module:get_option("s2sout_override");
|
||||
end
|
||||
|
||||
function attach(event)
|
||||
local session = event.session;
|
||||
|
||||
if s2s_overrides and s2s_overrides[event.host] then
|
||||
session.cert_chain_status = 'valid';
|
||||
session.cert_identity_status = 'valid';
|
||||
|
||||
return true;
|
||||
end
|
||||
end
|
||||
module:wrap_event('s2s-check-certificate', function (handlers, event_name, event_data)
|
||||
return attach(event_data);
|
||||
end);
|
202
roles/jitsi/files/prosody/modules/mod_client_proxy.lua
Normal file
202
roles/jitsi/files/prosody/modules/mod_client_proxy.lua
Normal file
@ -0,0 +1,202 @@
|
||||
if module:get_host_type() ~= "component" then
|
||||
error("proxy_component should be loaded as component", 0);
|
||||
end
|
||||
|
||||
local jid_split = require "util.jid".split;
|
||||
local jid_bare = require "util.jid".bare;
|
||||
local jid_prep = require "util.jid".prep;
|
||||
local st = require "util.stanza";
|
||||
local array = require "util.array";
|
||||
|
||||
local target_address = module:get_option_string("target_address");
|
||||
|
||||
sessions = array{};
|
||||
local sessions = sessions;
|
||||
|
||||
local function handle_target_presence(stanza)
|
||||
local type = stanza.attr.type;
|
||||
module:log("debug", "received presence from destination: %s", type)
|
||||
local _, _, resource = jid_split(stanza.attr.from);
|
||||
if type == "error" then
|
||||
-- drop all known sessions
|
||||
for k in pairs(sessions) do
|
||||
sessions[k] = nil
|
||||
end
|
||||
module:log(
|
||||
"debug",
|
||||
"received error presence, dropping all target sessions",
|
||||
resource
|
||||
)
|
||||
elseif type == "unavailable" then
|
||||
for k in pairs(sessions) do
|
||||
if sessions[k] == resource then
|
||||
sessions[k] = nil
|
||||
module:log(
|
||||
"debug",
|
||||
"dropped target session: %s",
|
||||
resource
|
||||
)
|
||||
break
|
||||
end
|
||||
end
|
||||
elseif not type then
|
||||
-- available
|
||||
local found = false;
|
||||
for k in pairs(sessions) do
|
||||
if sessions[k] == resource then
|
||||
found = true;
|
||||
break
|
||||
end
|
||||
end
|
||||
if not found then
|
||||
module:log(
|
||||
"debug",
|
||||
"registered new target session: %s",
|
||||
resource
|
||||
)
|
||||
sessions:push(resource)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function handle_from_target(stanza)
|
||||
local type = stanza.attr.type
|
||||
module:log(
|
||||
"debug",
|
||||
"non-presence stanza from target: name = %s, type = %s",
|
||||
stanza.name,
|
||||
type
|
||||
)
|
||||
if stanza.name == "iq" then
|
||||
if type == "error" or type == "result" then
|
||||
-- de-NAT message
|
||||
local _, _, denatted_to_unprepped = jid_split(stanza.attr.to);
|
||||
local denatted_to = jid_prep(denatted_to_unprepped);
|
||||
if not denatted_to then
|
||||
module:log(
|
||||
"debug",
|
||||
"cannot de-NAT stanza, invalid to: %s",
|
||||
denatted_to_unprepped
|
||||
)
|
||||
return
|
||||
end
|
||||
local denatted_from = module:get_host();
|
||||
|
||||
module:log(
|
||||
"debug",
|
||||
"de-NAT-ed stanza: from: %s -> %s, to: %s -> %s",
|
||||
stanza.attr.from,
|
||||
denatted_from,
|
||||
stanza.attr.to,
|
||||
denatted_to
|
||||
)
|
||||
|
||||
stanza.attr.from = denatted_from
|
||||
stanza.attr.to = denatted_to
|
||||
|
||||
module:send(stanza)
|
||||
else
|
||||
-- FIXME: we don’t support NATing outbund requests atm.
|
||||
module:send(st.error_reply(stanza, "cancel", "feature-not-implemented"))
|
||||
end
|
||||
elseif stanza.name == "message" then
|
||||
-- not implemented yet, we need a way to ensure that routing doesn’t
|
||||
-- break
|
||||
module:send(st.error_reply(stanza, "cancel", "feature-not-implemented"))
|
||||
end
|
||||
end
|
||||
|
||||
local function handle_to_target(stanza)
|
||||
local type = stanza.attr.type;
|
||||
module:log(
|
||||
"debug",
|
||||
"stanza to target: name = %s, type = %s",
|
||||
stanza.name, type
|
||||
)
|
||||
if stanza.name == "presence" then
|
||||
if type ~= "error" then
|
||||
module:send(st.error_reply(stanza, "cancel", "bad-request"))
|
||||
return
|
||||
end
|
||||
elseif stanza.name == "iq" then
|
||||
if type == "get" or type == "set" then
|
||||
if #sessions == 0 then
|
||||
-- no sessions available to send to
|
||||
module:log("debug", "no sessions to send to!")
|
||||
module:send(st.error_reply(stanza, "cancel", "service-unavailable"))
|
||||
return
|
||||
end
|
||||
|
||||
-- find a target session
|
||||
local target_session = sessions:random()
|
||||
local target = target_address .. "/" .. target_session
|
||||
|
||||
-- encode sender JID in resource
|
||||
local natted_from = module:get_host() .. "/" .. stanza.attr.from;
|
||||
|
||||
module:log(
|
||||
"debug",
|
||||
"NAT-ed stanza: from: %s -> %s, to: %s -> %s",
|
||||
stanza.attr.from,
|
||||
natted_from,
|
||||
stanza.attr.to,
|
||||
target
|
||||
)
|
||||
|
||||
stanza.attr.from = natted_from
|
||||
stanza.attr.to = target
|
||||
|
||||
module:send(stanza)
|
||||
end
|
||||
-- FIXME: handle and forward result/error correctly
|
||||
elseif stanza.name == "message" then
|
||||
-- not implemented yet, we need a way to ensure that routing doesn’t
|
||||
-- break
|
||||
module:send(st.error_reply(stanza, "cancel", "feature-not-implemented"))
|
||||
end
|
||||
end
|
||||
|
||||
local function stanza_handler(event)
|
||||
local origin, stanza = event.origin, event.stanza
|
||||
module:log("debug", "received stanza from %s session", origin.type)
|
||||
|
||||
local bare_from = jid_bare(stanza.attr.from);
|
||||
local _, _, to = jid_split(stanza.attr.to);
|
||||
if bare_from == target_address then
|
||||
-- from our target, to whom?
|
||||
if not to then
|
||||
-- directly to component
|
||||
if stanza.name == "presence" then
|
||||
handle_target_presence(stanza)
|
||||
else
|
||||
module:send(st.error_reply(stanza, "cancel", "bad-request"))
|
||||
return true
|
||||
end
|
||||
else
|
||||
-- to someone else
|
||||
handle_from_target(stanza)
|
||||
end
|
||||
else
|
||||
handle_to_target(stanza)
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
module:hook("iq/bare", stanza_handler, -1);
|
||||
module:hook("message/bare", stanza_handler, -1);
|
||||
module:hook("presence/bare", stanza_handler, -1);
|
||||
module:hook("iq/full", stanza_handler, -1);
|
||||
module:hook("message/full", stanza_handler, -1);
|
||||
module:hook("presence/full", stanza_handler, -1);
|
||||
module:hook("iq/host", stanza_handler, -1);
|
||||
module:hook("message/host", stanza_handler, -1);
|
||||
module:hook("presence/host", stanza_handler, -1);
|
||||
|
||||
module:log("debug", "loaded proxy on %s", module:get_host())
|
||||
|
||||
subscription_request = st.presence({
|
||||
type = "subscribe",
|
||||
to = target_address,
|
||||
from = module:get_host()}
|
||||
)
|
||||
module:send(subscription_request)
|
@ -0,0 +1,48 @@
|
||||
local it = require "util.iterators";
|
||||
local process_host_module = module:require "util".process_host_module;
|
||||
|
||||
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
|
||||
|
||||
-- Returns the meeting created timestamp form data.
|
||||
function getMeetingCreatedTSConfig(room)
|
||||
return {
|
||||
name = "muc#roominfo_created_timestamp";
|
||||
type = "text-single";
|
||||
label = "The meeting created_timestamp.";
|
||||
value = room.created_timestamp or "";
|
||||
};
|
||||
end
|
||||
|
||||
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 = string.format('%i', os.time() * 1000); -- Lua provides UTC time in seconds, so convert to milliseconds
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
process_host_module(main_muc_component_config, function(host_module, host)
|
||||
-- add meeting Id to the disco info requests to the room
|
||||
host_module:hook("muc-disco#info", function(event)
|
||||
table.insert(event.form, getMeetingCreatedTSConfig(event.room));
|
||||
end);
|
||||
|
||||
-- Marks the created timestamp in the room object
|
||||
host_module:hook("muc-occupant-joined", occupant_joined, -1);
|
||||
end);
|
||||
|
||||
-- DEPRECATED and will be removed, giving time for mobile clients to update
|
||||
local conference_duration_component
|
||||
= module:get_option_string("conference_duration_component", "conferenceduration."..module.host);
|
||||
if conference_duration_component then
|
||||
module:add_identity("component", "conference_duration", conference_duration_component);
|
||||
end
|
@ -0,0 +1,47 @@
|
||||
-- DEPRECATED and will be removed, giving time for mobile clients to update
|
||||
local st = require "util.stanza";
|
||||
local socket = require "socket";
|
||||
local json = require 'cjson.safe';
|
||||
local it = require "util.iterators";
|
||||
local process_host_module = module:require "util".process_host_module;
|
||||
|
||||
-- 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
|
||||
module:log("error", "No muc_component specified. No muc to operate on!");
|
||||
return;
|
||||
end
|
||||
|
||||
module: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
|
||||
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
|
||||
|
||||
process_host_module(muc_component_host, function(host_module, host)
|
||||
host_module:hook("muc-occupant-joined", occupant_joined, -1);
|
||||
end);
|
54
roles/jitsi/files/prosody/modules/mod_debug_traceback.lua
Normal file
54
roles/jitsi/files/prosody/modules/mod_debug_traceback.lua
Normal file
@ -0,0 +1,54 @@
|
||||
module:set_global();
|
||||
|
||||
local traceback = require "util.debug".traceback;
|
||||
local pposix = require "util.pposix";
|
||||
local os_date = os.date;
|
||||
local render_filename = require "util.interpolation".new("%b{}", function (s) return s; end, {
|
||||
yyyymmdd = function (t)
|
||||
return os_date("%Y%m%d", t);
|
||||
end;
|
||||
hhmmss = function (t)
|
||||
return os_date("%H%M%S", t);
|
||||
end;
|
||||
});
|
||||
|
||||
local count = 0;
|
||||
|
||||
local function get_filename(filename_template)
|
||||
filename_template = filename_template;
|
||||
return render_filename(filename_template, {
|
||||
paths = prosody.paths;
|
||||
pid = pposix.getpid();
|
||||
count = count;
|
||||
time = os.time();
|
||||
});
|
||||
end
|
||||
|
||||
local default_filename_template = "{paths.data}/traceback-{pid}-{count}.log";
|
||||
local filename_template = module:get_option_string("debug_traceback_filename", default_filename_template);
|
||||
local signal_name = module:get_option_string("debug_traceback_signal", "SIGUSR1");
|
||||
|
||||
function dump_traceback()
|
||||
module:log("info", "Received %s, writing traceback", signal_name);
|
||||
|
||||
local tb = traceback();
|
||||
module:fire_event("debug_traceback/triggered", { traceback = tb });
|
||||
|
||||
local f, err = io.open(get_filename(filename_template), "a+");
|
||||
if not f then
|
||||
module:log("error", "Unable to write traceback: %s", err);
|
||||
return;
|
||||
end
|
||||
f:write("-- Traceback generated at ", os.date("%b %d %H:%M:%S"), " --\n");
|
||||
f:write(tb, "\n");
|
||||
f:write("-- End of traceback --\n");
|
||||
f:close();
|
||||
count = count + 1;
|
||||
end
|
||||
|
||||
local mod_posix = module:depends("posix");
|
||||
if rawget(mod_posix, "features") and mod_posix.features.signal_events then
|
||||
module:hook("signal/"..signal_name, dump_traceback);
|
||||
else
|
||||
require"util.signal".signal(signal_name, dump_traceback);
|
||||
end
|
86
roles/jitsi/files/prosody/modules/mod_end_conference.lua
Normal file
86
roles/jitsi/files/prosody/modules/mod_end_conference.lua
Normal file
@ -0,0 +1,86 @@
|
||||
-- This module is added under the main virtual host domain
|
||||
--
|
||||
-- VirtualHost "jitmeet.example.com"
|
||||
-- modules_enabled = {
|
||||
-- "end_conference"
|
||||
-- }
|
||||
-- end_conference_component = "endconference.jitmeet.example.com"
|
||||
--
|
||||
-- Component "endconference.jitmeet.example.com" "end_conference"
|
||||
-- muc_component = muc.jitmeet.example.com
|
||||
--
|
||||
local get_room_by_name_and_subdomain = module:require 'util'.get_room_by_name_and_subdomain;
|
||||
|
||||
local END_CONFERENCE_REASON = 'The meeting has been terminated';
|
||||
|
||||
-- Since this file serves as both the host module and the component, we rely on the assumption that
|
||||
-- end_conference_component var would only be define for the host and not in the end_conference component
|
||||
local end_conference_component = module:get_option_string('end_conference_component');
|
||||
if end_conference_component then
|
||||
-- Advertise end conference so client can pick up the address and use it
|
||||
module:add_identity('component', 'end_conference', end_conference_component);
|
||||
return; -- nothing left to do if called as host module
|
||||
end
|
||||
|
||||
-- What follows is logic for the end_conference component
|
||||
|
||||
module:depends("jitsi_session");
|
||||
|
||||
local muc_component_host = module:get_option_string('muc_component');
|
||||
if muc_component_host == nil then
|
||||
module:log('error', 'No muc_component specified. No muc to operate on!');
|
||||
return;
|
||||
end
|
||||
|
||||
module:log('info', 'Starting end_conference for %s', muc_component_host);
|
||||
|
||||
-- receives messages from clients to the component to end a conference
|
||||
function on_message(event)
|
||||
local session = event.origin;
|
||||
|
||||
-- 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 not session or not session.jitsi_web_query_room then
|
||||
return false;
|
||||
end
|
||||
|
||||
local moderation_command = event.stanza:get_child('end_conference');
|
||||
|
||||
if moderation_command then
|
||||
-- get room name with tenant and find room
|
||||
local room = get_room_by_name_and_subdomain(session.jitsi_web_query_room, session.jitsi_web_query_prefix);
|
||||
|
||||
if not room then
|
||||
module:log('warn', 'No room found found for %s/%s',
|
||||
session.jitsi_web_query_prefix, session.jitsi_web_query_room);
|
||||
return false;
|
||||
end
|
||||
|
||||
-- check that the participant requesting is a moderator and is an occupant in the room
|
||||
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, room.jid);
|
||||
return false;
|
||||
end
|
||||
if occupant.role ~= 'moderator' then
|
||||
module:log('warn', 'Occupant %s is not moderator and not allowed this operation for %s', from, room.jid);
|
||||
return false;
|
||||
end
|
||||
|
||||
-- destroy the room
|
||||
room:destroy(nil, END_CONFERENCE_REASON);
|
||||
module:log('info', 'Room %s destroyed by occupant %s', room.jid, from);
|
||||
return true;
|
||||
end
|
||||
|
||||
-- return error
|
||||
return false
|
||||
end
|
||||
|
||||
|
||||
-- we will receive messages from the clients
|
||||
module:hook('message/host', on_message);
|
232
roles/jitsi/files/prosody/modules/mod_external_services.lua
Normal file
232
roles/jitsi/files/prosody/modules/mod_external_services.lua
Normal file
@ -0,0 +1,232 @@
|
||||
|
||||
local dt = require "util.datetime";
|
||||
local base64 = require "util.encodings".base64;
|
||||
local hashes = require "util.hashes";
|
||||
local st = require "util.stanza";
|
||||
local jid = require "util.jid";
|
||||
local array = require "util.array";
|
||||
local set = require "util.set";
|
||||
|
||||
local default_host = module:get_option_string("external_service_host", module.host);
|
||||
local default_port = module:get_option_number("external_service_port");
|
||||
local default_secret = module:get_option_string("external_service_secret");
|
||||
local default_ttl = module:get_option_number("external_service_ttl", 86400);
|
||||
|
||||
local configured_services = module:get_option_array("external_services", {});
|
||||
|
||||
local access = module:get_option_set("external_service_access", {});
|
||||
|
||||
-- https://tools.ietf.org/html/draft-uberti-behave-turn-rest-00
|
||||
local function behave_turn_rest_credentials(srv, item, secret)
|
||||
local ttl = default_ttl;
|
||||
if type(item.ttl) == "number" then
|
||||
ttl = item.ttl;
|
||||
end
|
||||
local expires = srv.expires or os.time() + ttl;
|
||||
local username;
|
||||
if type(item.username) == "string" then
|
||||
username = string.format("%d:%s", expires, item.username);
|
||||
else
|
||||
username = string.format("%d", expires);
|
||||
end
|
||||
srv.username = username;
|
||||
srv.password = base64.encode(hashes.hmac_sha1(secret, srv.username));
|
||||
end
|
||||
|
||||
local algorithms = {
|
||||
turn = behave_turn_rest_credentials;
|
||||
}
|
||||
|
||||
-- filter config into well-defined service records
|
||||
local function prepare(item)
|
||||
if type(item) ~= "table" then
|
||||
module:log("error", "Service definition is not a table: %q", item);
|
||||
return nil;
|
||||
end
|
||||
|
||||
local srv = {
|
||||
type = nil;
|
||||
transport = nil;
|
||||
host = default_host;
|
||||
port = default_port;
|
||||
username = nil;
|
||||
password = nil;
|
||||
restricted = nil;
|
||||
expires = nil;
|
||||
};
|
||||
|
||||
if type(item.type) == "string" then
|
||||
srv.type = item.type;
|
||||
else
|
||||
module:log("error", "Service missing mandatory 'type' field: %q", item);
|
||||
return nil;
|
||||
end
|
||||
if type(item.transport) == "string" then
|
||||
srv.transport = item.transport;
|
||||
end
|
||||
if type(item.host) == "string" then
|
||||
srv.host = item.host;
|
||||
end
|
||||
if type(item.port) == "number" then
|
||||
srv.port = item.port;
|
||||
end
|
||||
if type(item.username) == "string" then
|
||||
srv.username = item.username;
|
||||
end
|
||||
if type(item.password) == "string" then
|
||||
srv.password = item.password;
|
||||
srv.restricted = true;
|
||||
end
|
||||
if item.restricted == true then
|
||||
srv.restricted = true;
|
||||
end
|
||||
if type(item.expires) == "number" then
|
||||
srv.expires = item.expires;
|
||||
elseif type(item.ttl) == "number" then
|
||||
srv.expires = os.time() + item.ttl;
|
||||
end
|
||||
if (item.secret == true and default_secret) or type(item.secret) == "string" then
|
||||
local secret_cb = item.credentials_cb or algorithms[item.algorithm] or algorithms[srv.type];
|
||||
local secret = item.secret;
|
||||
if secret == true then
|
||||
secret = default_secret;
|
||||
end
|
||||
if secret_cb then
|
||||
secret_cb(srv, item, secret);
|
||||
srv.restricted = true;
|
||||
end
|
||||
end
|
||||
return srv;
|
||||
end
|
||||
|
||||
function module.load()
|
||||
-- Trigger errors on startup
|
||||
local services = configured_services / prepare;
|
||||
if #services == 0 then
|
||||
module:log("warn", "No services configured or all had errors");
|
||||
end
|
||||
end
|
||||
|
||||
-- Ensure only valid items are added in events
|
||||
local services_mt = {
|
||||
__index = getmetatable(array()).__index;
|
||||
__newindex = function (self, i, v)
|
||||
rawset(self, i, assert(prepare(v), "Invalid service entry added"));
|
||||
end;
|
||||
}
|
||||
|
||||
function get_services()
|
||||
local extras = module:get_host_items("external_service");
|
||||
local services = ( configured_services + extras ) / prepare;
|
||||
|
||||
setmetatable(services, services_mt);
|
||||
|
||||
return services;
|
||||
end
|
||||
|
||||
function services_xml(services, name, namespace)
|
||||
local reply = st.stanza(name or "services", { xmlns = namespace or "urn:xmpp:extdisco:2" });
|
||||
|
||||
for _, srv in ipairs(services) do
|
||||
reply:tag("service", {
|
||||
type = srv.type;
|
||||
transport = srv.transport;
|
||||
host = srv.host;
|
||||
port = srv.port and string.format("%d", srv.port) or nil;
|
||||
username = srv.username;
|
||||
password = srv.password;
|
||||
expires = srv.expires and dt.datetime(srv.expires) or nil;
|
||||
restricted = srv.restricted and "1" or nil;
|
||||
}):up();
|
||||
end
|
||||
|
||||
return reply;
|
||||
end
|
||||
|
||||
local function handle_services(event)
|
||||
local origin, stanza = event.origin, event.stanza;
|
||||
local action = stanza.tags[1];
|
||||
|
||||
local user_bare = jid.bare(stanza.attr.from);
|
||||
local user_host = jid.host(user_bare);
|
||||
if not ((access:empty() and origin.type == "c2s") or access:contains(user_bare) or access:contains(user_host)) then
|
||||
origin.send(st.error_reply(stanza, "auth", "forbidden"));
|
||||
return true;
|
||||
end
|
||||
|
||||
local services = get_services();
|
||||
|
||||
local requested_type = action.attr.type;
|
||||
if requested_type then
|
||||
services:filter(function(item)
|
||||
return item.type == requested_type;
|
||||
end);
|
||||
end
|
||||
|
||||
module:fire_event("external_service/services", {
|
||||
origin = origin;
|
||||
stanza = stanza;
|
||||
requested_type = requested_type;
|
||||
services = services;
|
||||
});
|
||||
|
||||
local reply = st.reply(stanza):add_child(services_xml(services, action.name, action.attr.xmlns));
|
||||
|
||||
origin.send(reply);
|
||||
return true;
|
||||
end
|
||||
|
||||
local function handle_credentials(event)
|
||||
local origin, stanza = event.origin, event.stanza;
|
||||
local action = stanza.tags[1];
|
||||
|
||||
if origin.type ~= "c2s" then
|
||||
origin.send(st.error_reply(stanza, "auth", "forbidden", "The 'port' and 'type' attributes are required."));
|
||||
return true;
|
||||
end
|
||||
|
||||
local services = get_services();
|
||||
services:filter(function (item)
|
||||
return item.restricted;
|
||||
end)
|
||||
|
||||
local requested_credentials = set.new();
|
||||
for service in action:childtags("service") do
|
||||
if not service.attr.type or not service.attr.host then
|
||||
origin.send(st.error_reply(stanza, "modify", "bad-request"));
|
||||
return true;
|
||||
end
|
||||
|
||||
requested_credentials:add(string.format("%s:%s:%d", service.attr.type, service.attr.host,
|
||||
tonumber(service.attr.port) or 0));
|
||||
end
|
||||
|
||||
module:fire_event("external_service/credentials", {
|
||||
origin = origin;
|
||||
stanza = stanza;
|
||||
requested_credentials = requested_credentials;
|
||||
services = services;
|
||||
});
|
||||
|
||||
services:filter(function (srv)
|
||||
local port_key = string.format("%s:%s:%d", srv.type, srv.host, srv.port or 0);
|
||||
local portless_key = string.format("%s:%s:%d", srv.type, srv.host, 0);
|
||||
return requested_credentials:contains(port_key) or requested_credentials:contains(portless_key);
|
||||
end);
|
||||
|
||||
local reply = st.reply(stanza):add_child(services_xml(services, action.name, action.attr.xmlns));
|
||||
|
||||
origin.send(reply);
|
||||
return true;
|
||||
end
|
||||
|
||||
-- XEP-0215 v0.7
|
||||
module:add_feature("urn:xmpp:extdisco:2");
|
||||
module:hook("iq-get/host/urn:xmpp:extdisco:2:services", handle_services);
|
||||
module:hook("iq-get/host/urn:xmpp:extdisco:2:credentials", handle_credentials);
|
||||
|
||||
-- COMPAT XEP-0215 v0.6
|
||||
-- Those still on the old version gets to deal with undefined attributes until they upgrade.
|
||||
module:add_feature("urn:xmpp:extdisco:1");
|
||||
module:hook("iq-get/host/urn:xmpp:extdisco:1:services", handle_services);
|
||||
module:hook("iq-get/host/urn:xmpp:extdisco:1:credentials", handle_credentials);
|
27
roles/jitsi/files/prosody/modules/mod_filter_iq_jibri.lua
Normal file
27
roles/jitsi/files/prosody/modules/mod_filter_iq_jibri.lua
Normal file
@ -0,0 +1,27 @@
|
||||
local st = require "util.stanza";
|
||||
local is_feature_allowed = module:require "util".is_feature_allowed;
|
||||
|
||||
-- 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
|
||||
if token == nil
|
||||
or not is_feature_allowed(session.jitsi_meet_context_features,
|
||||
(jibri.attr.recording_mode == 'file' and 'recording' or 'livestreaming')
|
||||
) then
|
||||
module:log("info",
|
||||
"Filtering jibri start recording, stanza:%s", tostring(stanza));
|
||||
session.send(st.error_reply(stanza, "auth", "forbidden"));
|
||||
return true;
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end);
|
261
roles/jitsi/files/prosody/modules/mod_filter_iq_rayo.lua
Normal file
261
roles/jitsi/files/prosody/modules/mod_filter_iq_rayo.lua
Normal file
@ -0,0 +1,261 @@
|
||||
local new_throttle = require "util.throttle".create;
|
||||
local st = require "util.stanza";
|
||||
|
||||
local token_util = module:require "token/util".new(module);
|
||||
local util = module:require 'util';
|
||||
local room_jid_match_rewrite = util.room_jid_match_rewrite;
|
||||
local is_feature_allowed = util.is_feature_allowed;
|
||||
local is_sip_jigasi = util.is_sip_jigasi;
|
||||
local get_room_from_jid = util.get_room_from_jid;
|
||||
local is_healthcheck_room = util.is_healthcheck_room;
|
||||
local process_host_module = util.process_host_module;
|
||||
local jid_bare = require "util.jid".bare;
|
||||
|
||||
local sessions = prosody.full_sessions;
|
||||
|
||||
local measure_drop = module:measure('drop', 'counter');
|
||||
|
||||
local main_muc_component_host = module:get_option_string('main_muc');
|
||||
if main_muc_component_host == nil then
|
||||
module:log('error', 'main_muc not configured. Cannot proceed.');
|
||||
return;
|
||||
end
|
||||
local main_muc_service;
|
||||
|
||||
-- no token configuration but required
|
||||
if token_util == nil then
|
||||
module:log("error", "no token configuration but it is required");
|
||||
return;
|
||||
end
|
||||
|
||||
local um_is_admin = require 'core.usermanager'.is_admin;
|
||||
local function is_admin(jid)
|
||||
return um_is_admin(jid, module.host);
|
||||
end
|
||||
|
||||
-- The maximum number of simultaneous calls,
|
||||
-- and also the maximum number of new calls per minute that a session is allowed to create.
|
||||
local limit_outgoing_calls;
|
||||
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
|
||||
|
||||
local feature = dial.attr.to == 'jitsi_meet_transcribe' and 'transcription' or 'outbound-call';
|
||||
local is_session_allowed = is_feature_allowed(session.jitsi_meet_context_features, feature);
|
||||
|
||||
-- if current user is not allowed, but was granted moderation by a user
|
||||
-- that is allowed by its features we want to allow it
|
||||
local is_granting_session_allowed = false;
|
||||
if (session.granted_jitsi_meet_context_features) then
|
||||
is_granting_session_allowed = is_feature_allowed(session.granted_jitsi_meet_context_features, feature);
|
||||
end
|
||||
|
||||
if (token == nil
|
||||
or roomName == nil
|
||||
or not token_util:verify_room(session, room_jid_match_rewrite(roomName))
|
||||
or not (is_session_allowed or is_granting_session_allowed))
|
||||
then
|
||||
module:log("warn", "Filtering stanza dial, stanza:%s", tostring(stanza));
|
||||
session.send(st.error_reply(stanza, "auth", "forbidden"));
|
||||
return true;
|
||||
end
|
||||
|
||||
-- we get current user_id or group, or the one from the granted one
|
||||
-- so guests and the user that granted rights are sharing same limit, as guest can be without token
|
||||
local user_id, group_id = nil, session.jitsi_meet_context_group;
|
||||
if session.jitsi_meet_context_user then
|
||||
user_id = session.jitsi_meet_context_user["id"];
|
||||
else
|
||||
user_id = session.granted_jitsi_meet_context_user_id;
|
||||
group_id = session.granted_jitsi_meet_context_group_id;
|
||||
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(user_id, group_id) >= limit_outgoing_calls
|
||||
then
|
||||
module:log("warn",
|
||||
"Filtering stanza dial, stanza:%s, outgoing calls limit reached", tostring(stanza));
|
||||
measure_drop(1);
|
||||
session.send(st.error_reply(stanza, "cancel", "resource-constraint"));
|
||||
return true;
|
||||
end
|
||||
end
|
||||
|
||||
-- now lets insert token information if any
|
||||
if session and user_id then
|
||||
-- First remove any 'header' element if it already
|
||||
-- exists, so it cannot be spoofed by a client
|
||||
stanza:maptags(
|
||||
function(tag)
|
||||
if tag.name == "header"
|
||||
and (tag.attr.name == OUT_INITIATOR_USER_ATTR_NAME
|
||||
or tag.attr.name == OUT_INITIATOR_GROUP_ATTR_NAME) then
|
||||
return nil
|
||||
end
|
||||
return tag
|
||||
end
|
||||
)
|
||||
|
||||
local dial = stanza:get_child('dial', 'urn:xmpp:rayo:1');
|
||||
-- adds initiator user id from token
|
||||
dial:tag("header", {
|
||||
xmlns = "urn:xmpp:rayo:1",
|
||||
name = OUT_INITIATOR_USER_ATTR_NAME,
|
||||
value = user_id });
|
||||
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;
|
||||
local rooms = main_muc_service.live_rooms();
|
||||
|
||||
-- now lets iterate over rooms and occupants and search for
|
||||
-- call initiated by the user
|
||||
for room in rooms do
|
||||
for _, occupant in room:each_occupant() do
|
||||
for _, presence in occupant:each_session() do
|
||||
|
||||
local initiator = is_sip_jigasi(presence);
|
||||
|
||||
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
|
||||
|
||||
return count;
|
||||
end
|
||||
|
||||
module:hook_global('config-reloaded', load_config);
|
||||
|
||||
function process_set_affiliation(event)
|
||||
local actor, affiliation, jid, previous_affiliation, room
|
||||
= event.actor, event.affiliation, event.jid, event.previous_affiliation, event.room;
|
||||
local actor_session = sessions[actor];
|
||||
|
||||
if is_admin(jid) or is_healthcheck_room(room.jid) or not actor or not previous_affiliation
|
||||
or not actor_session or not actor_session.jitsi_meet_context_features then
|
||||
return;
|
||||
end
|
||||
|
||||
local occupant;
|
||||
for _, o in room:each_occupant() do
|
||||
if o.bare_jid == jid then
|
||||
occupant = o;
|
||||
end
|
||||
end
|
||||
|
||||
if not occupant then
|
||||
return;
|
||||
end
|
||||
|
||||
local occupant_session = sessions[occupant.jid];
|
||||
if not occupant_session then
|
||||
return;
|
||||
end
|
||||
|
||||
if previous_affiliation == 'none' and affiliation == 'owner' then
|
||||
occupant_session.granted_jitsi_meet_context_features = actor_session.jitsi_meet_context_features;
|
||||
occupant_session.granted_jitsi_meet_context_user_id = actor_session.jitsi_meet_context_user["id"];
|
||||
occupant_session.granted_jitsi_meet_context_group_id = actor_session.jitsi_meet_context_group;
|
||||
elseif previous_affiliation == 'owner' and ( affiliation == 'member' or affiliation == 'none' ) then
|
||||
occupant_session.granted_jitsi_meet_context_features = nil;
|
||||
occupant_session.granted_jitsi_meet_context_user_id = nil;
|
||||
occupant_session.granted_jitsi_meet_context_group_id = nil;
|
||||
end
|
||||
end
|
||||
|
||||
function process_main_muc_loaded(main_muc, host_module)
|
||||
module:log('debug', 'Main muc loaded');
|
||||
|
||||
main_muc_service = main_muc;
|
||||
module:log("info", "Hook to muc events on %s", main_muc_component_host);
|
||||
host_module:hook("muc-pre-set-affiliation", process_set_affiliation);
|
||||
end
|
||||
|
||||
process_host_module(main_muc_component_host, function(host_module, host)
|
||||
local muc_module = prosody.hosts[host].modules.muc;
|
||||
|
||||
if muc_module then
|
||||
process_main_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_main_muc_loaded(prosody.hosts[host].modules.muc, host_module);
|
||||
end
|
||||
end);
|
||||
end
|
||||
end);
|
280
roles/jitsi/files/prosody/modules/mod_firewall/actions.lib.lua
Normal file
280
roles/jitsi/files/prosody/modules/mod_firewall/actions.lib.lua
Normal file
@ -0,0 +1,280 @@
|
||||
local unpack = table.unpack or unpack;
|
||||
|
||||
local interpolation = require "util.interpolation";
|
||||
local template = interpolation.new("%b$$", function (s) return ("%q"):format(s) end);
|
||||
|
||||
--luacheck: globals meta idsafe
|
||||
local action_handlers = {};
|
||||
|
||||
|
||||
-- Takes an XML string and returns a code string that builds that stanza
|
||||
-- using st.stanza()
|
||||
local function compile_xml(data)
|
||||
local code = {};
|
||||
local first, short_close = true, nil;
|
||||
for tagline, text in data:gmatch("<([^>]+)>([^<]*)") do
|
||||
if tagline:sub(-1,-1) == "/" then
|
||||
tagline = tagline:sub(1, -2);
|
||||
short_close = true;
|
||||
end
|
||||
if tagline:sub(1,1) == "/" then
|
||||
code[#code+1] = (":up()");
|
||||
else
|
||||
local name, attr = tagline:match("^(%S*)%s*(.*)$");
|
||||
local attr_str = {};
|
||||
for k, _, v in attr:gmatch("(%S+)=([\"'])([^%2]-)%2") do
|
||||
if #attr_str == 0 then
|
||||
table.insert(attr_str, ", { ");
|
||||
else
|
||||
table.insert(attr_str, ", ");
|
||||
end
|
||||
if k:find("^%a%w*$") then
|
||||
table.insert(attr_str, string.format("%s = %q", k, v));
|
||||
else
|
||||
table.insert(attr_str, string.format("[%q] = %q", k, v));
|
||||
end
|
||||
end
|
||||
if #attr_str > 0 then
|
||||
table.insert(attr_str, " }");
|
||||
end
|
||||
if first then
|
||||
code[#code+1] = (string.format("st.stanza(%q %s)", name, #attr_str>0 and table.concat(attr_str) or ", nil"));
|
||||
first = nil;
|
||||
else
|
||||
code[#code+1] = (string.format(":tag(%q%s)", name, table.concat(attr_str)));
|
||||
end
|
||||
end
|
||||
if text and text:find("%S") then
|
||||
code[#code+1] = (string.format(":text(%q)", text));
|
||||
elseif short_close then
|
||||
short_close = nil;
|
||||
code[#code+1] = (":up()");
|
||||
end
|
||||
end
|
||||
return table.concat(code, "");
|
||||
end
|
||||
|
||||
function action_handlers.PASS()
|
||||
return "do return pass_return end"
|
||||
end
|
||||
|
||||
function action_handlers.DROP()
|
||||
return "do return true end";
|
||||
end
|
||||
|
||||
function action_handlers.DEFAULT()
|
||||
return "do return false end";
|
||||
end
|
||||
|
||||
function action_handlers.RETURN()
|
||||
return "do return end"
|
||||
end
|
||||
|
||||
function action_handlers.STRIP(tag_desc)
|
||||
local code = {};
|
||||
local name, xmlns = tag_desc:match("^(%S+) (.+)$");
|
||||
if not name then
|
||||
name, xmlns = tag_desc, nil;
|
||||
end
|
||||
if name == "*" then
|
||||
name = nil;
|
||||
end
|
||||
code[#code+1] = ("local stanza_xmlns = stanza.attr.xmlns; ");
|
||||
code[#code+1] = "stanza:maptags(function (tag) if ";
|
||||
if name then
|
||||
code[#code+1] = ("tag.name == %q and "):format(name);
|
||||
end
|
||||
if xmlns then
|
||||
code[#code+1] = ("(tag.attr.xmlns or stanza_xmlns) == %q "):format(xmlns);
|
||||
else
|
||||
code[#code+1] = ("tag.attr.xmlns == stanza_xmlns ");
|
||||
end
|
||||
code[#code+1] = "then return nil; end return tag; end );";
|
||||
return table.concat(code);
|
||||
end
|
||||
|
||||
function action_handlers.INJECT(tag)
|
||||
return "stanza:add_child("..compile_xml(tag)..")", { "st" };
|
||||
end
|
||||
|
||||
local error_types = {
|
||||
["bad-request"] = "modify";
|
||||
["conflict"] = "cancel";
|
||||
["feature-not-implemented"] = "cancel";
|
||||
["forbidden"] = "auth";
|
||||
["gone"] = "cancel";
|
||||
["internal-server-error"] = "cancel";
|
||||
["item-not-found"] = "cancel";
|
||||
["jid-malformed"] = "modify";
|
||||
["not-acceptable"] = "modify";
|
||||
["not-allowed"] = "cancel";
|
||||
["not-authorized"] = "auth";
|
||||
["payment-required"] = "auth";
|
||||
["policy-violation"] = "modify";
|
||||
["recipient-unavailable"] = "wait";
|
||||
["redirect"] = "modify";
|
||||
["registration-required"] = "auth";
|
||||
["remote-server-not-found"] = "cancel";
|
||||
["remote-server-timeout"] = "wait";
|
||||
["resource-constraint"] = "wait";
|
||||
["service-unavailable"] = "cancel";
|
||||
["subscription-required"] = "auth";
|
||||
["undefined-condition"] = "cancel";
|
||||
["unexpected-request"] = "wait";
|
||||
};
|
||||
|
||||
|
||||
local function route_modify(make_new, to, drop)
|
||||
local reroute, deps = "session.send(newstanza)", { "st" };
|
||||
if to then
|
||||
reroute = ("newstanza.attr.to = %q; core_post_stanza(session, newstanza)"):format(to);
|
||||
deps[#deps+1] = "core_post_stanza";
|
||||
end
|
||||
return ([[do local newstanza = st.%s; %s;%s end]])
|
||||
:format(make_new, reroute, drop and " return true" or ""), deps;
|
||||
end
|
||||
|
||||
function action_handlers.BOUNCE(with)
|
||||
local error = with and with:match("^%S+") or "service-unavailable";
|
||||
local error_type = error:match(":(%S+)");
|
||||
if not error_type then
|
||||
error_type = error_types[error] or "cancel";
|
||||
else
|
||||
error = error:match("^[^:]+");
|
||||
end
|
||||
error, error_type = string.format("%q", error), string.format("%q", error_type);
|
||||
local text = with and with:match(" %((.+)%)$");
|
||||
if text then
|
||||
text = string.format("%q", text);
|
||||
else
|
||||
text = "nil";
|
||||
end
|
||||
local route_modify_code, deps = route_modify(("error_reply(stanza, %s, %s, %s)"):format(error_type, error, text), nil, true);
|
||||
deps[#deps+1] = "type";
|
||||
deps[#deps+1] = "name";
|
||||
return [[if type == "error" or (name == "iq" and type == "result") then return true; end -- Don't reply to 'error' stanzas, or iq results
|
||||
]]..route_modify_code, deps;
|
||||
end
|
||||
|
||||
function action_handlers.REDIRECT(where)
|
||||
return route_modify("clone(stanza)", where, true);
|
||||
end
|
||||
|
||||
function action_handlers.COPY(where)
|
||||
return route_modify("clone(stanza)", where, false);
|
||||
end
|
||||
|
||||
function action_handlers.REPLY(with)
|
||||
return route_modify(("reply(stanza):body(%q)"):format(with));
|
||||
end
|
||||
|
||||
function action_handlers.FORWARD(where)
|
||||
local code = [[
|
||||
local newstanza = st.stanza("message", { to = %q, from = current_host }):tag("forwarded", { xmlns = "urn:xmpp:forward:0" });
|
||||
local tmp_stanza = st.clone(stanza); tmp_stanza.attr.xmlns = "jabber:client"; newstanza:add_child(tmp_stanza);
|
||||
core_post_stanza(session, newstanza);
|
||||
]];
|
||||
return code:format(where), { "core_post_stanza", "current_host" };
|
||||
end
|
||||
|
||||
function action_handlers.LOG(string)
|
||||
local level = string:match("^%[(%a+)%]") or "info";
|
||||
string = string:gsub("^%[%a+%] ?", "");
|
||||
local meta_deps = {};
|
||||
local code = meta(("(session.log or log)(%q, '%%s', %q);"):format(level, string), meta_deps);
|
||||
return code, meta_deps;
|
||||
end
|
||||
|
||||
function action_handlers.RULEDEP(dep)
|
||||
return "", { dep };
|
||||
end
|
||||
|
||||
function action_handlers.EVENT(name)
|
||||
return ("fire_event(%q, event)"):format(name);
|
||||
end
|
||||
|
||||
function action_handlers.JUMP_EVENT(name)
|
||||
return ("do return fire_event(%q, event); end"):format(name);
|
||||
end
|
||||
|
||||
function action_handlers.JUMP_CHAIN(name)
|
||||
return template([[do
|
||||
local ret = fire_event($chain_event$, event);
|
||||
if ret ~= nil then
|
||||
if ret == false then
|
||||
log("debug", "Chain %q accepted stanza (ret %s)", $chain_name$, tostring(ret));
|
||||
return pass_return;
|
||||
end
|
||||
log("debug", "Chain %q rejected stanza (ret %s)", $chain_name$, tostring(ret));
|
||||
return ret;
|
||||
end
|
||||
end]], { chain_event = "firewall/chains/"..name, chain_name = name });
|
||||
end
|
||||
|
||||
function action_handlers.MARK_ORIGIN(name)
|
||||
return [[session.firewall_marked_]]..idsafe(name)..[[ = current_timestamp;]], { "timestamp" };
|
||||
end
|
||||
|
||||
function action_handlers.UNMARK_ORIGIN(name)
|
||||
return [[session.firewall_marked_]]..idsafe(name)..[[ = nil;]]
|
||||
end
|
||||
|
||||
function action_handlers.MARK_USER(name)
|
||||
return ([[if session.username and session.host == current_host then
|
||||
fire_event("firewall/marked/user", {
|
||||
username = session.username;
|
||||
mark = %q;
|
||||
timestamp = current_timestamp;
|
||||
});
|
||||
else
|
||||
log("warn", "Attempt to MARK a remote user - only local users may be marked");
|
||||
end]]):format(assert(idsafe(name), "Invalid characters in mark name: "..name)), {
|
||||
"current_host";
|
||||
"timestamp";
|
||||
};
|
||||
end
|
||||
|
||||
function action_handlers.UNMARK_USER(name)
|
||||
return ([[if session.username and session.host == current_host then
|
||||
fire_event("firewall/unmarked/user", {
|
||||
username = session.username;
|
||||
mark = %q;
|
||||
});
|
||||
else
|
||||
log("warn", "Attempt to UNMARK a remote user - only local users may be marked");
|
||||
end]]):format(assert(idsafe(name), "Invalid characters in mark name: "..name));
|
||||
end
|
||||
|
||||
function action_handlers.ADD_TO(spec)
|
||||
local list_name, value = spec:match("(%S+) (.+)");
|
||||
local meta_deps = {};
|
||||
value = meta(("%q"):format(value), meta_deps);
|
||||
return ("list_%s:add(%s);"):format(list_name, value), { "list:"..list_name, unpack(meta_deps) };
|
||||
end
|
||||
|
||||
function action_handlers.UNSUBSCRIBE_SENDER()
|
||||
return "rostermanager.unsubscribed(to_node, to_host, bare_from);\
|
||||
rostermanager.roster_push(to_node, to_host, bare_from);\
|
||||
core_post_stanza(session, st.presence({ from = bare_to, to = bare_from, type = \"unsubscribed\" }));",
|
||||
{ "rostermanager", "core_post_stanza", "st", "split_to", "bare_to", "bare_from" };
|
||||
end
|
||||
|
||||
function action_handlers.REPORT_TO(spec)
|
||||
local where, reason, text = spec:match("^%s*(%S+) *(%S*) *(.*)$");
|
||||
if reason == "spam" then
|
||||
reason = "urn:xmpp:reporting:spam";
|
||||
elseif reason == "abuse" or not reason then
|
||||
reason = "urn:xmpp:reporting:abuse";
|
||||
end
|
||||
local code = [[
|
||||
local newstanza = st.stanza("message", { to = %q, from = current_host }):tag("forwarded", { xmlns = "urn:xmpp:forward:0" });
|
||||
local tmp_stanza = st.clone(stanza); tmp_stanza.attr.xmlns = "jabber:client"; newstanza:add_child(tmp_stanza):up();
|
||||
newstanza:tag("report", { xmlns = "urn:xmpp:reporting:1", reason = %q })
|
||||
do local text = %q; if text ~= "" then newstanza:text_tag("text", text); end end
|
||||
newstanza:up();
|
||||
core_post_stanza(session, newstanza);
|
||||
]];
|
||||
return code:format(where, reason, text), { "core_post_stanza", "current_host", "st" };
|
||||
end
|
||||
|
||||
return action_handlers;
|
@ -0,0 +1,384 @@
|
||||
--luacheck: globals meta idsafe
|
||||
local condition_handlers = {};
|
||||
|
||||
local jid = require "util.jid";
|
||||
local unpack = table.unpack or unpack;
|
||||
|
||||
-- Helper to convert user-input strings (yes/true//no/false) to a bool
|
||||
local function string_to_boolean(s)
|
||||
s = s:lower();
|
||||
return s == "yes" or s == "true";
|
||||
end
|
||||
|
||||
-- Return a code string for a condition that checks whether the contents
|
||||
-- of variable with the name 'name' matches any of the values in the
|
||||
-- comma/space/pipe delimited list 'values'.
|
||||
local function compile_comparison_list(name, values)
|
||||
local conditions = {};
|
||||
for value in values:gmatch("[^%s,|]+") do
|
||||
table.insert(conditions, ("%s == %q"):format(name, value));
|
||||
end
|
||||
return table.concat(conditions, " or ");
|
||||
end
|
||||
|
||||
function condition_handlers.KIND(kind)
|
||||
assert(kind, "Expected stanza kind to match against");
|
||||
return compile_comparison_list("name", kind), { "name" };
|
||||
end
|
||||
|
||||
local wildcard_equivs = { ["*"] = ".*", ["?"] = "." };
|
||||
|
||||
local function compile_jid_match_part(part, match)
|
||||
if not match then
|
||||
return part.." == nil";
|
||||
end
|
||||
local pattern = match:match("^<(.*)>$");
|
||||
if pattern then
|
||||
if pattern == "*" then
|
||||
return part;
|
||||
end
|
||||
if pattern:find("^<.*>$") then
|
||||
pattern = pattern:match("^<(.*)>$");
|
||||
else
|
||||
pattern = pattern:gsub("%p", "%%%0"):gsub("%%(%p)", wildcard_equivs);
|
||||
end
|
||||
return ("(%s and %s:find(%q))"):format(part, part, "^"..pattern.."$");
|
||||
else
|
||||
return ("%s == %q"):format(part, match);
|
||||
end
|
||||
end
|
||||
|
||||
local function compile_jid_match(which, match_jid)
|
||||
local match_node, match_host, match_resource = jid.split(match_jid);
|
||||
local conditions = {};
|
||||
conditions[#conditions+1] = compile_jid_match_part(which.."_node", match_node);
|
||||
conditions[#conditions+1] = compile_jid_match_part(which.."_host", match_host);
|
||||
if match_resource then
|
||||
conditions[#conditions+1] = compile_jid_match_part(which.."_resource", match_resource);
|
||||
end
|
||||
return table.concat(conditions, " and ");
|
||||
end
|
||||
|
||||
function condition_handlers.TO(to)
|
||||
return compile_jid_match("to", to), { "split_to" };
|
||||
end
|
||||
|
||||
function condition_handlers.FROM(from)
|
||||
return compile_jid_match("from", from), { "split_from" };
|
||||
end
|
||||
|
||||
function condition_handlers.FROM_FULL_JID()
|
||||
return "not "..compile_jid_match_part("from_resource", nil), { "split_from" };
|
||||
end
|
||||
|
||||
function condition_handlers.FROM_EXACTLY(from)
|
||||
local metadeps = {};
|
||||
return ("from == %s"):format(metaq(from, metadeps)), { "from", unpack(metadeps) };
|
||||
end
|
||||
|
||||
function condition_handlers.TO_EXACTLY(to)
|
||||
local metadeps = {};
|
||||
return ("to == %s"):format(metaq(to, metadeps)), { "to", unpack(metadeps) };
|
||||
end
|
||||
|
||||
function condition_handlers.TO_SELF()
|
||||
-- Intentionally not using 'to' here, as that defaults to bare JID when nil
|
||||
return ("stanza.attr.to == nil");
|
||||
end
|
||||
|
||||
function condition_handlers.TYPE(type)
|
||||
assert(type, "Expected 'type' value to match against");
|
||||
return compile_comparison_list("(type or (name == 'message' and 'normal') or (name == 'presence' and 'available'))", type), { "type", "name" };
|
||||
end
|
||||
|
||||
local function zone_check(zone, which)
|
||||
local zone_var = zone;
|
||||
if zone == "$local" then zone_var = "_local" end
|
||||
local which_not = which == "from" and "to" or "from";
|
||||
return ("(zone_%s[%s_host] or zone_%s[%s] or zone_%s[bare_%s]) "
|
||||
.."and not(zone_%s[%s_host] or zone_%s[%s] or zone_%s[bare_%s])"
|
||||
)
|
||||
:format(zone_var, which, zone_var, which, zone_var, which,
|
||||
zone_var, which_not, zone_var, which_not, zone_var, which_not), {
|
||||
"split_to", "split_from", "bare_to", "bare_from", "zone:"..zone
|
||||
};
|
||||
end
|
||||
|
||||
function condition_handlers.ENTERING(zone)
|
||||
return zone_check(zone, "to");
|
||||
end
|
||||
|
||||
function condition_handlers.LEAVING(zone)
|
||||
return zone_check(zone, "from");
|
||||
end
|
||||
|
||||
-- IN ROSTER? (parameter is deprecated)
|
||||
function condition_handlers.IN_ROSTER(yes_no)
|
||||
local in_roster_requirement = string_to_boolean(yes_no or "yes"); -- COMPAT w/ older scripts
|
||||
return "not "..(in_roster_requirement and "not" or "").." roster_entry", { "roster_entry" };
|
||||
end
|
||||
|
||||
function condition_handlers.IN_ROSTER_GROUP(group)
|
||||
return ("not not (roster_entry and roster_entry.groups[%q])"):format(group), { "roster_entry" };
|
||||
end
|
||||
|
||||
function condition_handlers.SUBSCRIBED()
|
||||
return "(bare_to == bare_from or to_node and rostermanager.is_contact_subscribed(to_node, to_host, bare_from))",
|
||||
{ "rostermanager", "split_to", "bare_to", "bare_from" };
|
||||
end
|
||||
|
||||
function condition_handlers.PENDING_SUBSCRIPTION_FROM_SENDER()
|
||||
return "(bare_to == bare_from or to_node and rostermanager.is_contact_pending_in(to_node, to_host, bare_from))",
|
||||
{ "rostermanager", "split_to", "bare_to", "bare_from" };
|
||||
end
|
||||
|
||||
function condition_handlers.PAYLOAD(payload_ns)
|
||||
return ("stanza:get_child(nil, %q)"):format(payload_ns);
|
||||
end
|
||||
|
||||
function condition_handlers.INSPECT(path)
|
||||
if path:find("=") then
|
||||
local query, match_type, value = path:match("(.-)([~/$]*)=(.*)");
|
||||
if not(query:match("#$") or query:match("@[^/]+")) then
|
||||
error("Stanza path does not return a string (append # for text content or @name for value of named attribute)", 0);
|
||||
end
|
||||
local meta_deps = {};
|
||||
local quoted_value = ("%q"):format(value);
|
||||
if match_type:find("$", 1, true) then
|
||||
match_type = match_type:gsub("%$", "");
|
||||
quoted_value = meta(quoted_value, meta_deps);
|
||||
end
|
||||
if match_type == "~" then -- Lua pattern match
|
||||
return ("(stanza:find(%q) or ''):match(%s)"):format(query, quoted_value), meta_deps;
|
||||
elseif match_type == "/" then -- find literal substring
|
||||
return ("(stanza:find(%q) or ''):find(%s, 1, true)"):format(query, quoted_value), meta_deps;
|
||||
elseif match_type == "" then -- exact match
|
||||
return ("stanza:find(%q) == %s"):format(query, quoted_value), meta_deps;
|
||||
else
|
||||
error("Unrecognised comparison '"..match_type.."='", 0);
|
||||
end
|
||||
end
|
||||
return ("stanza:find(%q)"):format(path);
|
||||
end
|
||||
|
||||
function condition_handlers.FROM_GROUP(group_name)
|
||||
return ("group_contains(%q, bare_from)"):format(group_name), { "group_contains", "bare_from" };
|
||||
end
|
||||
|
||||
function condition_handlers.TO_GROUP(group_name)
|
||||
return ("group_contains(%q, bare_to)"):format(group_name), { "group_contains", "bare_to" };
|
||||
end
|
||||
|
||||
function condition_handlers.CROSSING_GROUPS(group_names)
|
||||
local code = {};
|
||||
for group_name in group_names:gmatch("([^, ][^,]+)") do
|
||||
group_name = group_name:match("^%s*(.-)%s*$"); -- Trim leading/trailing whitespace
|
||||
-- Just check that's it is crossing from outside group to inside group
|
||||
table.insert(code, ("(group_contains(%q, bare_to) and group_contains(%q, bare_from))"):format(group_name, group_name))
|
||||
end
|
||||
return "not "..table.concat(code, " or "), { "group_contains", "bare_to", "bare_from" };
|
||||
end
|
||||
|
||||
-- COMPAT w/0.12: Deprecated
|
||||
function condition_handlers.FROM_ADMIN_OF(host)
|
||||
return ("is_admin(bare_from, %s)"):format(host ~= "*" and metaq(host) or nil), { "is_admin", "bare_from" };
|
||||
end
|
||||
|
||||
-- COMPAT w/0.12: Deprecated
|
||||
function condition_handlers.TO_ADMIN_OF(host)
|
||||
return ("is_admin(bare_to, %s)"):format(host ~= "*" and metaq(host) or nil), { "is_admin", "bare_to" };
|
||||
end
|
||||
|
||||
-- COMPAT w/0.12: Deprecated
|
||||
function condition_handlers.FROM_ADMIN()
|
||||
return ("is_admin(bare_from, current_host)"), { "is_admin", "bare_from", "current_host" };
|
||||
end
|
||||
|
||||
-- COMPAT w/0.12: Deprecated
|
||||
function condition_handlers.TO_ADMIN()
|
||||
return ("is_admin(bare_to, current_host)"), { "is_admin", "bare_to", "current_host" };
|
||||
end
|
||||
|
||||
-- MAY: permission_to_check
|
||||
function condition_handlers.MAY(permission_to_check)
|
||||
return ("module:may(%q, event)"):format(permission_to_check);
|
||||
end
|
||||
|
||||
function condition_handlers.TO_ROLE(role_name)
|
||||
return ("get_jid_role(bare_to, current_host) == %q"):format(role_name), { "get_jid_role", "current_host", "bare_to" };
|
||||
end
|
||||
|
||||
function condition_handlers.FROM_ROLE(role_name)
|
||||
return ("get_jid_role(bare_from, current_host) == %q"):format(role_name), { "get_jid_role", "current_host", "bare_from" };
|
||||
end
|
||||
|
||||
local day_numbers = { sun = 0, mon = 2, tue = 3, wed = 4, thu = 5, fri = 6, sat = 7 };
|
||||
|
||||
local function current_time_check(op, hour, minute)
|
||||
hour, minute = tonumber(hour), tonumber(minute);
|
||||
local adj_op = op == "<" and "<" or ">="; -- Start time inclusive, end time exclusive
|
||||
if minute == 0 then
|
||||
return "(current_hour"..adj_op..hour..")";
|
||||
else
|
||||
return "((current_hour"..op..hour..") or (current_hour == "..hour.." and current_minute"..adj_op..minute.."))";
|
||||
end
|
||||
end
|
||||
|
||||
local function resolve_day_number(day_name)
|
||||
return assert(day_numbers[day_name:sub(1,3):lower()], "Unknown day name: "..day_name);
|
||||
end
|
||||
|
||||
function condition_handlers.DAY(days)
|
||||
local conditions = {};
|
||||
for day_range in days:gmatch("[^,]+") do
|
||||
local day_start, day_end = day_range:match("(%a+)%s*%-%s*(%a+)");
|
||||
if day_start and day_end then
|
||||
local day_start_num, day_end_num = resolve_day_number(day_start), resolve_day_number(day_end);
|
||||
local op = "and";
|
||||
if day_end_num < day_start_num then
|
||||
op = "or";
|
||||
end
|
||||
table.insert(conditions, ("current_day >= %d %s current_day <= %d"):format(day_start_num, op, day_end_num));
|
||||
elseif day_range:find("%a") then
|
||||
local day = resolve_day_number(day_range:match("%a+"));
|
||||
table.insert(conditions, "current_day == "..day);
|
||||
else
|
||||
error("Unable to parse day/day range: "..day_range);
|
||||
end
|
||||
end
|
||||
assert(#conditions>0, "Expected a list of days or day ranges");
|
||||
return "("..table.concat(conditions, ") or (")..")", { "time:day" };
|
||||
end
|
||||
|
||||
function condition_handlers.TIME(ranges)
|
||||
local conditions = {};
|
||||
for range in ranges:gmatch("([^,]+)") do
|
||||
local clause = {};
|
||||
range = range:lower()
|
||||
:gsub("(%d+):?(%d*) *am", function (h, m) return tostring(tonumber(h)%12)..":"..(tonumber(m) or "00"); end)
|
||||
:gsub("(%d+):?(%d*) *pm", function (h, m) return tostring(tonumber(h)%12+12)..":"..(tonumber(m) or "00"); end);
|
||||
local start_hour, start_minute = range:match("(%d+):(%d+) *%-");
|
||||
local end_hour, end_minute = range:match("%- *(%d+):(%d+)");
|
||||
local op = tonumber(start_hour) > tonumber(end_hour) and " or " or " and ";
|
||||
if start_hour and end_hour then
|
||||
table.insert(clause, current_time_check(">", start_hour, start_minute));
|
||||
table.insert(clause, current_time_check("<", end_hour, end_minute));
|
||||
end
|
||||
if #clause == 0 then
|
||||
error("Unable to parse time range: "..range);
|
||||
end
|
||||
table.insert(conditions, "("..table.concat(clause, " "..op.." ")..")");
|
||||
end
|
||||
return table.concat(conditions, " or "), { "time:hour,min" };
|
||||
end
|
||||
|
||||
function condition_handlers.LIMIT(spec)
|
||||
local name, param = spec:match("^(%w+) on (.+)$");
|
||||
local meta_deps = {};
|
||||
|
||||
if not name then
|
||||
name = spec:match("^%w+$");
|
||||
if not name then
|
||||
error("Unable to parse LIMIT specification");
|
||||
end
|
||||
else
|
||||
param = meta(("%q"):format(param), meta_deps);
|
||||
end
|
||||
|
||||
if not param then
|
||||
return ("not global_throttle_%s:poll(1)"):format(name), { "globalthrottle:"..name, unpack(meta_deps) };
|
||||
end
|
||||
return ("not multi_throttle_%s:poll_on(%s, 1)"):format(name, param), { "multithrottle:"..name, unpack(meta_deps) };
|
||||
end
|
||||
|
||||
function condition_handlers.ORIGIN_MARKED(name_and_time)
|
||||
local name, time = name_and_time:match("^%s*([%w_]+)%s+%(([^)]+)s%)%s*$");
|
||||
if not name then
|
||||
name = name_and_time:match("^%s*([%w_]+)%s*$");
|
||||
end
|
||||
if not name then
|
||||
error("Error parsing mark name, see documentation for usage examples");
|
||||
end
|
||||
if time then
|
||||
return ("(current_timestamp - (session.firewall_marked_%s or 0)) < %d"):format(idsafe(name), tonumber(time)), { "timestamp" };
|
||||
end
|
||||
return ("not not session.firewall_marked_"..idsafe(name));
|
||||
end
|
||||
|
||||
function condition_handlers.USER_MARKED(name_and_time)
|
||||
local name, time = name_and_time:match("^%s*([%w_]+)%s+%(([^)]+)s%)%s*$");
|
||||
if not name then
|
||||
name = name_and_time:match("^%s*([%w_]+)%s*$");
|
||||
end
|
||||
if not name then
|
||||
error("Error parsing mark name, see documentation for usage examples");
|
||||
end
|
||||
if time then
|
||||
return ([[(
|
||||
current_timestamp - (session.firewall_marks and session.firewall_marks.%s or 0)
|
||||
) < %d]]):format(idsafe(name), tonumber(time)), { "timestamp" };
|
||||
end
|
||||
return ("not not (session.firewall_marks and session.firewall_marks."..idsafe(name)..")");
|
||||
end
|
||||
|
||||
function condition_handlers.SENT_DIRECTED_PRESENCE_TO_SENDER()
|
||||
return "not not (session.directed and session.directed[from])", { "from" };
|
||||
end
|
||||
|
||||
-- TO FULL JID?
|
||||
function condition_handlers.TO_FULL_JID()
|
||||
return "not not full_sessions[to]", { "to", "full_sessions" };
|
||||
end
|
||||
|
||||
-- CHECK LIST: spammers contains $<@from>
|
||||
function condition_handlers.CHECK_LIST(list_condition)
|
||||
local list_name, expr = list_condition:match("(%S+) contains (.+)$");
|
||||
if not (list_name and expr) then
|
||||
error("Error parsing list check, syntax: LISTNAME contains EXPRESSION");
|
||||
end
|
||||
local meta_deps = {};
|
||||
expr = meta(("%q"):format(expr), meta_deps);
|
||||
return ("list_%s:contains(%s) == true"):format(list_name, expr), { "list:"..list_name, unpack(meta_deps) };
|
||||
end
|
||||
|
||||
-- SCAN: body for word in badwords
|
||||
function condition_handlers.SCAN(scan_expression)
|
||||
local search_name, pattern_name, list_name = scan_expression:match("(%S+) for (%S+) in (%S+)$");
|
||||
if not (search_name) then
|
||||
error("Error parsing SCAN expression, syntax: SEARCH for PATTERN in LIST");
|
||||
end
|
||||
return ("scan_list(list_%s, %s)"):format(
|
||||
list_name,
|
||||
"tokens_"..search_name.."_"..pattern_name
|
||||
), {
|
||||
"scan_list",
|
||||
"tokens:"..search_name.."-"..pattern_name, "list:"..list_name
|
||||
};
|
||||
end
|
||||
|
||||
-- COUNT: lines in body < 10
|
||||
local valid_comp_ops = { [">"] = ">", ["<"] = "<", ["="] = "==", ["=="] = "==", ["<="] = "<=", [">="] = ">=" };
|
||||
function condition_handlers.COUNT(count_expression)
|
||||
local pattern_name, search_name, comparator_expression = count_expression:match("(%S+) in (%S+) (.+)$");
|
||||
if not (pattern_name) then
|
||||
error("Error parsing COUNT expression, syntax: PATTERN in SEARCH COMPARATOR");
|
||||
end
|
||||
local value;
|
||||
comparator_expression = comparator_expression:gsub("%d+", function (value_string)
|
||||
value = tonumber(value_string);
|
||||
return "";
|
||||
end);
|
||||
if not value then
|
||||
error("Error parsing COUNT expression, expected value");
|
||||
end
|
||||
local comp_op = comparator_expression:gsub("%s+", "");
|
||||
assert(valid_comp_ops[comp_op], "Error parsing COUNT expression, unknown comparison operator: "..comp_op);
|
||||
return ("it_count(search_%s:gmatch(pattern_%s)) %s %d"):format(
|
||||
search_name, pattern_name, comp_op, value
|
||||
), {
|
||||
"it_count",
|
||||
"search:"..search_name, "pattern:"..pattern_name
|
||||
};
|
||||
end
|
||||
|
||||
return condition_handlers;
|
@ -0,0 +1,335 @@
|
||||
|
||||
-- Name arguments are unused here
|
||||
-- luacheck: ignore 212
|
||||
|
||||
local definition_handlers = {};
|
||||
|
||||
local http = require "net.http";
|
||||
local timer = require "util.timer";
|
||||
local set = require"util.set";
|
||||
local new_throttle = require "util.throttle".create;
|
||||
local hashes = require "util.hashes";
|
||||
local jid = require "util.jid";
|
||||
local lfs = require "lfs";
|
||||
|
||||
local multirate_cache_size = module:get_option_number("firewall_multirate_cache_limit", 1000);
|
||||
|
||||
function definition_handlers.ZONE(zone_name, zone_members)
|
||||
local zone_member_list = {};
|
||||
for member in zone_members:gmatch("[^, ]+") do
|
||||
zone_member_list[#zone_member_list+1] = member;
|
||||
end
|
||||
return set.new(zone_member_list)._items;
|
||||
end
|
||||
|
||||
-- Helper function used by RATE handler
|
||||
local function evict_only_unthrottled(name, throttle)
|
||||
throttle:update();
|
||||
-- Check whether the throttle is at max balance (i.e. totally safe to forget about it)
|
||||
if throttle.balance < throttle.max then
|
||||
-- Not safe to forget
|
||||
return false;
|
||||
end
|
||||
end
|
||||
|
||||
function definition_handlers.RATE(name, line)
|
||||
local rate = assert(tonumber(line:match("([%d.]+)")), "Unable to parse rate");
|
||||
local burst = tonumber(line:match("%(%s*burst%s+([%d.]+)%s*%)")) or 1;
|
||||
local max_throttles = tonumber(line:match("%(%s*entries%s+([%d]+)%s*%)")) or multirate_cache_size;
|
||||
local deny_when_full = not line:match("%(allow overflow%)");
|
||||
return {
|
||||
single = function ()
|
||||
return new_throttle(rate*burst, burst);
|
||||
end;
|
||||
|
||||
multi = function ()
|
||||
local cache = require "util.cache".new(max_throttles, deny_when_full and evict_only_unthrottled or nil);
|
||||
return {
|
||||
poll_on = function (_, key, amount)
|
||||
assert(key, "no key");
|
||||
local throttle = cache:get(key);
|
||||
if not throttle then
|
||||
throttle = new_throttle(rate*burst, burst);
|
||||
if not cache:set(key, throttle) then
|
||||
module:log("warn", "Multirate '%s' has hit its maximum number of active throttles (%d), denying new events", name, max_throttles);
|
||||
return false;
|
||||
end
|
||||
end
|
||||
return throttle:poll(amount);
|
||||
end;
|
||||
}
|
||||
end;
|
||||
};
|
||||
end
|
||||
|
||||
local list_backends = {
|
||||
-- %LIST name: memory (limit: number)
|
||||
memory = {
|
||||
init = function (self, type, opts)
|
||||
if opts.limit then
|
||||
local have_cache_lib, cache_lib = pcall(require, "util.cache");
|
||||
if not have_cache_lib then
|
||||
error("In-memory lists with a size limit require Prosody 0.10");
|
||||
end
|
||||
self.cache = cache_lib.new((assert(tonumber(opts.limit), "Invalid list limit")));
|
||||
if not self.cache.table then
|
||||
error("In-memory lists with a size limit require a newer version of Prosody 0.10");
|
||||
end
|
||||
self.items = self.cache:table();
|
||||
else
|
||||
self.items = {};
|
||||
end
|
||||
end;
|
||||
add = function (self, item)
|
||||
self.items[item] = true;
|
||||
end;
|
||||
remove = function (self, item)
|
||||
self.items[item] = nil;
|
||||
end;
|
||||
contains = function (self, item)
|
||||
return self.items[item] == true;
|
||||
end;
|
||||
};
|
||||
|
||||
-- %LIST name: http://example.com/ (ttl: number, pattern: pat, hash: sha1)
|
||||
http = {
|
||||
init = function (self, url, opts)
|
||||
local poll_interval = assert(tonumber(opts.ttl or "3600"), "invalid ttl for <"..url.."> (expected number of seconds)");
|
||||
local pattern = opts.pattern or "([^\r\n]+)\r?\n";
|
||||
assert(pcall(string.match, "", pattern), "invalid pattern for <"..url..">");
|
||||
if opts.hash then
|
||||
assert(opts.hash:match("^%w+$") and type(hashes[opts.hash]) == "function", "invalid hash function: "..opts.hash);
|
||||
self.hash_function = hashes[opts.hash];
|
||||
end
|
||||
local etag;
|
||||
local failure_count = 0;
|
||||
local retry_intervals = { 60, 120, 300 };
|
||||
-- By default only check the certificate if net.http supports SNI
|
||||
local sni_supported = http.feature and http.features.sni;
|
||||
local insecure = false;
|
||||
if opts.checkcert == "never" then
|
||||
insecure = true;
|
||||
elseif (opts.checkcert == nil or opts.checkcert == "when-sni") and not sni_supported then
|
||||
insecure = false;
|
||||
end
|
||||
local function update_list()
|
||||
http.request(url, {
|
||||
insecure = insecure;
|
||||
headers = {
|
||||
["If-None-Match"] = etag;
|
||||
};
|
||||
}, function (body, code, response)
|
||||
local next_poll = poll_interval;
|
||||
if code == 200 and body then
|
||||
etag = response.headers.etag;
|
||||
local items = {};
|
||||
for entry in body:gmatch(pattern) do
|
||||
items[entry] = true;
|
||||
end
|
||||
self.items = items;
|
||||
module:log("debug", "Fetched updated list from <%s>", url);
|
||||
elseif code == 304 then
|
||||
module:log("debug", "List at <%s> is unchanged", url);
|
||||
elseif code == 0 or (code >= 400 and code <=599) then
|
||||
module:log("warn", "Failed to fetch list from <%s>: %d %s", url, code, tostring(body));
|
||||
failure_count = failure_count + 1;
|
||||
next_poll = retry_intervals[failure_count] or retry_intervals[#retry_intervals];
|
||||
end
|
||||
if next_poll > 0 then
|
||||
timer.add_task(next_poll+math.random(0, 60), update_list);
|
||||
end
|
||||
end);
|
||||
end
|
||||
update_list();
|
||||
end;
|
||||
add = function ()
|
||||
end;
|
||||
remove = function ()
|
||||
end;
|
||||
contains = function (self, item)
|
||||
if self.hash_function then
|
||||
item = self.hash_function(item);
|
||||
end
|
||||
return self.items and self.items[item] == true;
|
||||
end;
|
||||
};
|
||||
|
||||
-- %LIST: file:/path/to/file
|
||||
file = {
|
||||
init = function (self, file_spec, opts)
|
||||
local n, items = 0, {};
|
||||
self.items = items;
|
||||
local filename = file_spec:gsub("^file:", "");
|
||||
if opts.missing == "ignore" and not lfs.attributes(filename, "mode") then
|
||||
module:log("debug", "Ignoring missing list file: %s", filename);
|
||||
return;
|
||||
end
|
||||
local file, err = io.open(filename);
|
||||
if not file then
|
||||
module:log("warn", "Failed to open list from %s: %s", filename, err);
|
||||
return;
|
||||
else
|
||||
for line in file:lines() do
|
||||
if not items[line] then
|
||||
n = n + 1;
|
||||
items[line] = true;
|
||||
end
|
||||
end
|
||||
end
|
||||
module:log("debug", "Loaded %d items from %s", n, filename);
|
||||
end;
|
||||
add = function (self, item)
|
||||
self.items[item] = true;
|
||||
end;
|
||||
remove = function (self, item)
|
||||
self.items[item] = nil;
|
||||
end;
|
||||
contains = function (self, item)
|
||||
return self.items and self.items[item] == true;
|
||||
end;
|
||||
};
|
||||
|
||||
-- %LIST: pubsub:pubsub.example.com/node
|
||||
-- TODO or the actual URI scheme? Bit overkill maybe?
|
||||
-- TODO Publish items back to the service?
|
||||
-- Step 1: Receiving pubsub events and storing them in the list
|
||||
-- We'll start by using only the item id.
|
||||
-- TODO Invent some custom schema for this? Needed for just a set of strings?
|
||||
pubsubitemid = {
|
||||
init = function(self, pubsub_spec, opts)
|
||||
local service_addr, node = pubsub_spec:match("^pubsubitemid:([^/]*)/(.*)");
|
||||
if not service_addr then
|
||||
module:log("warn", "Invalid list specification (expected 'pubsubitemid:<service>/<node>', got: '%s')", pubsub_spec);
|
||||
return;
|
||||
end
|
||||
module:depends("pubsub_subscription");
|
||||
module:add_item("pubsub-subscription", {
|
||||
service = service_addr;
|
||||
node = node;
|
||||
on_subscribed = function ()
|
||||
self.items = {};
|
||||
end;
|
||||
on_item = function (event)
|
||||
self:add(event.item.attr.id);
|
||||
end;
|
||||
on_retract = function (event)
|
||||
self:remove(event.item.attr.id);
|
||||
end;
|
||||
on_purge = function ()
|
||||
self.items = {};
|
||||
end;
|
||||
on_unsubscribed = function ()
|
||||
self.items = nil;
|
||||
end;
|
||||
on_delete= function ()
|
||||
self.items = nil;
|
||||
end;
|
||||
});
|
||||
-- TODO Initial fetch? Or should mod_pubsub_subscription do this?
|
||||
end;
|
||||
add = function (self, item)
|
||||
if self.items then
|
||||
self.items[item] = true;
|
||||
end
|
||||
end;
|
||||
remove = function (self, item)
|
||||
if self.items then
|
||||
self.items[item] = nil;
|
||||
end
|
||||
end;
|
||||
contains = function (self, item)
|
||||
return self.items and self.items[item] == true;
|
||||
end;
|
||||
};
|
||||
};
|
||||
list_backends.https = list_backends.http;
|
||||
|
||||
local normalize_functions = {
|
||||
upper = string.upper, lower = string.lower;
|
||||
md5 = hashes.md5, sha1 = hashes.sha1, sha256 = hashes.sha256;
|
||||
prep = jid.prep, bare = jid.bare;
|
||||
};
|
||||
|
||||
local function wrap_list_method(list_method, filter)
|
||||
return function (self, item)
|
||||
return list_method(self, filter(item));
|
||||
end
|
||||
end
|
||||
|
||||
local function create_list(list_backend, list_def, opts)
|
||||
if not list_backends[list_backend] then
|
||||
error("Unknown list type '"..list_backend.."'", 0);
|
||||
end
|
||||
local list = setmetatable({}, { __index = list_backends[list_backend] });
|
||||
if list.init then
|
||||
list:init(list_def, opts);
|
||||
end
|
||||
if opts.filter then
|
||||
local filters = {};
|
||||
for func_name in opts.filter:gmatch("[%w_]+") do
|
||||
if func_name == "log" then
|
||||
table.insert(filters, function (s)
|
||||
--print("&&&&&", s);
|
||||
module:log("debug", "Checking list <%s> for: %s", list_def, s);
|
||||
return s;
|
||||
end);
|
||||
else
|
||||
assert(normalize_functions[func_name], "Unknown list filter: "..func_name);
|
||||
table.insert(filters, normalize_functions[func_name]);
|
||||
end
|
||||
end
|
||||
|
||||
local filter;
|
||||
local n = #filters;
|
||||
if n == 1 then
|
||||
filter = filters[1];
|
||||
else
|
||||
function filter(s)
|
||||
for i = 1, n do
|
||||
s = filters[i](s or "");
|
||||
end
|
||||
return s;
|
||||
end
|
||||
end
|
||||
|
||||
list.add = wrap_list_method(list.add, filter);
|
||||
list.remove = wrap_list_method(list.remove, filter);
|
||||
list.contains = wrap_list_method(list.contains, filter);
|
||||
end
|
||||
return list;
|
||||
end
|
||||
|
||||
--[[
|
||||
%LIST spammers: memory (source: /etc/spammers.txt)
|
||||
|
||||
%LIST spammers: memory (source: /etc/spammers.txt)
|
||||
|
||||
|
||||
%LIST spammers: http://example.com/blacklist.txt
|
||||
]]
|
||||
|
||||
function definition_handlers.LIST(list_name, list_definition)
|
||||
local list_backend = list_definition:match("^%w+");
|
||||
local opts = {};
|
||||
local opt_string = list_definition:match("^%S+%s+%((.+)%)");
|
||||
if opt_string then
|
||||
for opt_k, opt_v in opt_string:gmatch("(%w+): ?([^,]+)") do
|
||||
opts[opt_k] = opt_v;
|
||||
end
|
||||
end
|
||||
return create_list(list_backend, list_definition:match("^%S+"), opts);
|
||||
end
|
||||
|
||||
function definition_handlers.PATTERN(name, pattern)
|
||||
local ok, err = pcall(string.match, "", pattern);
|
||||
if not ok then
|
||||
error("Invalid pattern '"..name.."': "..err);
|
||||
end
|
||||
return pattern;
|
||||
end
|
||||
|
||||
function definition_handlers.SEARCH(name, pattern)
|
||||
return pattern;
|
||||
end
|
||||
|
||||
return definition_handlers;
|
35
roles/jitsi/files/prosody/modules/mod_firewall/marks.lib.lua
Normal file
35
roles/jitsi/files/prosody/modules/mod_firewall/marks.lib.lua
Normal file
@ -0,0 +1,35 @@
|
||||
local mark_storage = module:open_store("firewall_marks");
|
||||
local mark_map_storage = module:open_store("firewall_marks", "map");
|
||||
|
||||
local user_sessions = prosody.hosts[module.host].sessions;
|
||||
|
||||
module:hook("firewall/marked/user", function (event)
|
||||
local user = user_sessions[event.username];
|
||||
local marks = user and user.firewall_marks;
|
||||
if user and not marks then
|
||||
-- Load marks from storage to cache on the user object
|
||||
marks = mark_storage:get(event.username) or {};
|
||||
user.firewall_marks = marks; --luacheck: ignore 122
|
||||
end
|
||||
if marks then
|
||||
marks[event.mark] = event.timestamp;
|
||||
end
|
||||
local ok, err = mark_map_storage:set(event.username, event.mark, event.timestamp);
|
||||
if not ok then
|
||||
module:log("error", "Failed to mark user %q with %q: %s", event.username, event.mark, err);
|
||||
end
|
||||
return true;
|
||||
end, -1);
|
||||
|
||||
module:hook("firewall/unmarked/user", function (event)
|
||||
local user = user_sessions[event.username];
|
||||
local marks = user and user.firewall_marks;
|
||||
if marks then
|
||||
marks[event.mark] = nil;
|
||||
end
|
||||
local ok, err = mark_map_storage:set(event.username, event.mark, nil);
|
||||
if not ok then
|
||||
module:log("error", "Failed to unmark user %q with %q: %s", event.username, event.mark, err);
|
||||
end
|
||||
return true;
|
||||
end, -1);
|
784
roles/jitsi/files/prosody/modules/mod_firewall/mod_firewall.lua
Normal file
784
roles/jitsi/files/prosody/modules/mod_firewall/mod_firewall.lua
Normal file
@ -0,0 +1,784 @@
|
||||
|
||||
local lfs = require "lfs";
|
||||
local resolve_relative_path = require "core.configmanager".resolve_relative_path;
|
||||
local envload = require "util.envload".envload;
|
||||
local logger = require "util.logger".init;
|
||||
local it = require "util.iterators";
|
||||
local set = require "util.set";
|
||||
|
||||
local have_features, features = pcall(require, "core.features");
|
||||
features = have_features and features.available or set.new();
|
||||
|
||||
-- [definition_type] = definition_factory(param)
|
||||
local definitions = module:shared("definitions");
|
||||
|
||||
-- When a definition instance has been instantiated, it lives here
|
||||
-- [definition_type][definition_name] = definition_object
|
||||
local active_definitions = {
|
||||
ZONE = {
|
||||
-- Default zone that includes all local hosts
|
||||
["$local"] = setmetatable({}, { __index = prosody.hosts });
|
||||
};
|
||||
};
|
||||
|
||||
local default_chains = {
|
||||
preroute = {
|
||||
type = "event";
|
||||
priority = 0.1;
|
||||
"pre-message/bare", "pre-message/full", "pre-message/host";
|
||||
"pre-presence/bare", "pre-presence/full", "pre-presence/host";
|
||||
"pre-iq/bare", "pre-iq/full", "pre-iq/host";
|
||||
};
|
||||
deliver = {
|
||||
type = "event";
|
||||
priority = 0.1;
|
||||
"message/bare", "message/full", "message/host";
|
||||
"presence/bare", "presence/full", "presence/host";
|
||||
"iq/bare", "iq/full", "iq/host";
|
||||
};
|
||||
deliver_remote = {
|
||||
type = "event"; "route/remote";
|
||||
priority = 0.1;
|
||||
};
|
||||
};
|
||||
|
||||
local extra_chains = module:get_option("firewall_extra_chains", {});
|
||||
|
||||
local chains = {};
|
||||
for k,v in pairs(default_chains) do
|
||||
chains[k] = v;
|
||||
end
|
||||
for k,v in pairs(extra_chains) do
|
||||
chains[k] = v;
|
||||
end
|
||||
|
||||
-- Returns the input if it is safe to be used as a variable name, otherwise nil
|
||||
function idsafe(name)
|
||||
return name:match("^%a[%w_]*$");
|
||||
end
|
||||
|
||||
local meta_funcs = {
|
||||
bare = function (code)
|
||||
return "jid_bare("..code..")", {"jid_bare"};
|
||||
end;
|
||||
node = function (code)
|
||||
return "(jid_split("..code.."))", {"jid_split"};
|
||||
end;
|
||||
host = function (code)
|
||||
return "(select(2, jid_split("..code..")))", {"jid_split"};
|
||||
end;
|
||||
resource = function (code)
|
||||
return "(select(3, jid_split("..code..")))", {"jid_split"};
|
||||
end;
|
||||
};
|
||||
|
||||
-- Run quoted (%q) strings through this to allow them to contain code. e.g.: LOG=Received: $(stanza:top_tag())
|
||||
function meta(s, deps, extra)
|
||||
return (s:gsub("$(%b())", function (expr)
|
||||
expr = expr:gsub("\\(.)", "%1");
|
||||
return [["..tostring(]]..expr..[[).."]];
|
||||
end)
|
||||
:gsub("$(%b<>)", function (expr)
|
||||
expr = expr:sub(2,-2);
|
||||
local default = "<undefined>";
|
||||
expr = expr:gsub("||(%b\"\")$", function (default_string)
|
||||
default = stripslashes(default_string:sub(2,-2));
|
||||
return "";
|
||||
end);
|
||||
local func_chain = expr:match("|[%w|]+$");
|
||||
if func_chain then
|
||||
expr = expr:sub(1, -1-#func_chain);
|
||||
end
|
||||
local code;
|
||||
if expr:match("^@") then
|
||||
-- Skip stanza:find() for simple attribute lookup
|
||||
local attr_name = expr:sub(2);
|
||||
if deps and (attr_name == "to" or attr_name == "from" or attr_name == "type") then
|
||||
-- These attributes may be cached in locals
|
||||
code = attr_name;
|
||||
table.insert(deps, attr_name);
|
||||
else
|
||||
code = "stanza.attr["..("%q"):format(attr_name).."]";
|
||||
end
|
||||
elseif expr:match("^%w+#$") then
|
||||
code = ("stanza:get_child_text(%q)"):format(expr:sub(1, -2));
|
||||
else
|
||||
code = ("stanza:find(%q)"):format(expr);
|
||||
end
|
||||
if func_chain then
|
||||
for func_name in func_chain:gmatch("|(%w+)") do
|
||||
-- to/from are already available in local variables, use those if possible
|
||||
if (code == "to" or code == "from") and func_name == "bare" then
|
||||
code = "bare_"..code;
|
||||
table.insert(deps, code);
|
||||
elseif (code == "to" or code == "from") and (func_name == "node" or func_name == "host" or func_name == "resource") then
|
||||
table.insert(deps, "split_"..code);
|
||||
code = code.."_"..func_name;
|
||||
else
|
||||
assert(meta_funcs[func_name], "unknown function: "..func_name);
|
||||
local new_code, new_deps = meta_funcs[func_name](code);
|
||||
code = new_code;
|
||||
if new_deps and #new_deps > 0 then
|
||||
assert(deps, "function not supported here: "..func_name);
|
||||
for _, dep in ipairs(new_deps) do
|
||||
table.insert(deps, dep);
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return "\"..tostring("..code.." or "..("%q"):format(default)..")..\"";
|
||||
end)
|
||||
:gsub("$$(%a+)", extra or {})
|
||||
:gsub([[^""%.%.]], "")
|
||||
:gsub([[%.%.""$]], ""));
|
||||
end
|
||||
|
||||
function metaq(s, ...)
|
||||
return meta(("%q"):format(s), ...);
|
||||
end
|
||||
|
||||
local escape_chars = {
|
||||
a = "\a", b = "\b", f = "\f", n = "\n", r = "\r", t = "\t",
|
||||
v = "\v", ["\\"] = "\\", ["\""] = "\"", ["\'"] = "\'"
|
||||
};
|
||||
function stripslashes(s)
|
||||
return (s:gsub("\\(.)", escape_chars));
|
||||
end
|
||||
|
||||
-- Dependency locations:
|
||||
-- <type lib>
|
||||
-- <type global>
|
||||
-- function handler()
|
||||
-- <local deps>
|
||||
-- if <conditions> then
|
||||
-- <actions>
|
||||
-- end
|
||||
-- end
|
||||
|
||||
local available_deps = {
|
||||
st = { global_code = [[local st = require "util.stanza";]]};
|
||||
it = { global_code = [[local it = require "util.iterators";]]};
|
||||
it_count = { global_code = [[local it_count = it.count;]], depends = { "it" } };
|
||||
current_host = { global_code = [[local current_host = module.host;]] };
|
||||
jid_split = {
|
||||
global_code = [[local jid_split = require "util.jid".split;]];
|
||||
};
|
||||
jid_bare = {
|
||||
global_code = [[local jid_bare = require "util.jid".bare;]];
|
||||
};
|
||||
to = { local_code = [[local to = stanza.attr.to or jid_bare(session.full_jid);]]; depends = { "jid_bare" } };
|
||||
from = { local_code = [[local from = stanza.attr.from;]] };
|
||||
type = { local_code = [[local type = stanza.attr.type;]] };
|
||||
name = { local_code = [[local name = stanza.name;]] };
|
||||
split_to = { -- The stanza's split to address
|
||||
depends = { "jid_split", "to" };
|
||||
local_code = [[local to_node, to_host, to_resource = jid_split(to);]];
|
||||
};
|
||||
split_from = { -- The stanza's split from address
|
||||
depends = { "jid_split", "from" };
|
||||
local_code = [[local from_node, from_host, from_resource = jid_split(from);]];
|
||||
};
|
||||
bare_to = { depends = { "jid_bare", "to" }, local_code = "local bare_to = jid_bare(to)"};
|
||||
bare_from = { depends = { "jid_bare", "from" }, local_code = "local bare_from = jid_bare(from)"};
|
||||
group_contains = {
|
||||
global_code = [[local group_contains = module:depends("groups").group_contains]];
|
||||
};
|
||||
is_admin = require"core.usermanager".is_admin and { global_code = [[local is_admin = require "core.usermanager".is_admin;]]} or nil;
|
||||
get_jid_role = require "core.usermanager".get_jid_role and { global_code = [[local get_jid_role = require "core.usermanager".get_jid_role;]] } or nil;
|
||||
core_post_stanza = { global_code = [[local core_post_stanza = prosody.core_post_stanza;]] };
|
||||
zone = { global_code = function (zone)
|
||||
local var = zone;
|
||||
if var == "$local" then
|
||||
var = "_local"; -- See #1090
|
||||
else
|
||||
assert(idsafe(var), "Invalid zone name: "..zone);
|
||||
end
|
||||
return ("local zone_%s = zones[%q] or {};"):format(var, zone);
|
||||
end };
|
||||
date_time = { global_code = [[local os_date = os.date]]; local_code = [[local current_date_time = os_date("*t");]] };
|
||||
time = { local_code = function (what)
|
||||
local defs = {};
|
||||
for field in what:gmatch("%a+") do
|
||||
table.insert(defs, ("local current_%s = current_date_time.%s;"):format(field, field));
|
||||
end
|
||||
return table.concat(defs, " ");
|
||||
end, depends = { "date_time" }; };
|
||||
timestamp = { global_code = [[local get_time = require "socket".gettime;]]; local_code = [[local current_timestamp = get_time();]]; };
|
||||
globalthrottle = {
|
||||
global_code = function (throttle)
|
||||
assert(idsafe(throttle), "Invalid rate limit name: "..throttle);
|
||||
assert(active_definitions.RATE[throttle], "Unknown rate limit: "..throttle);
|
||||
return ("local global_throttle_%s = rates.%s:single();"):format(throttle, throttle);
|
||||
end;
|
||||
};
|
||||
multithrottle = {
|
||||
global_code = function (throttle)
|
||||
assert(pcall(require, "util.cache"), "Using LIMIT with 'on' requires Prosody 0.10 or higher");
|
||||
assert(idsafe(throttle), "Invalid rate limit name: "..throttle);
|
||||
assert(active_definitions.RATE[throttle], "Unknown rate limit: "..throttle);
|
||||
return ("local multi_throttle_%s = rates.%s:multi();"):format(throttle, throttle);
|
||||
end;
|
||||
};
|
||||
full_sessions = {
|
||||
global_code = [[local full_sessions = prosody.full_sessions;]];
|
||||
};
|
||||
rostermanager = {
|
||||
global_code = [[local rostermanager = require "core.rostermanager";]];
|
||||
};
|
||||
roster_entry = {
|
||||
local_code = [[local roster_entry = (to_node and rostermanager.load_roster(to_node, to_host) or {})[bare_from];]];
|
||||
depends = { "rostermanager", "split_to", "bare_from" };
|
||||
};
|
||||
list = { global_code = function (list)
|
||||
assert(idsafe(list), "Invalid list name: "..list);
|
||||
assert(active_definitions.LIST[list], "Unknown list: "..list);
|
||||
return ("local list_%s = lists[%q];"):format(list, list);
|
||||
end
|
||||
};
|
||||
search = {
|
||||
local_code = function (search_name)
|
||||
local search_path = assert(active_definitions.SEARCH[search_name], "Undefined search path: "..search_name);
|
||||
return ("local search_%s = tostring(stanza:find(%q) or \"\")"):format(search_name, search_path);
|
||||
end;
|
||||
};
|
||||
pattern = {
|
||||
local_code = function (pattern_name)
|
||||
local pattern = assert(active_definitions.PATTERN[pattern_name], "Undefined pattern: "..pattern_name);
|
||||
return ("local pattern_%s = %q"):format(pattern_name, pattern);
|
||||
end;
|
||||
};
|
||||
tokens = {
|
||||
local_code = function (search_and_pattern)
|
||||
local search_name, pattern_name = search_and_pattern:match("^([^%-]+)-(.+)$");
|
||||
local code = ([[local tokens_%s_%s = {};
|
||||
if search_%s then
|
||||
for s in search_%s:gmatch(pattern_%s) do
|
||||
tokens_%s_%s[s] = true;
|
||||
end
|
||||
end
|
||||
]]):format(search_name, pattern_name, search_name, search_name, pattern_name, search_name, pattern_name);
|
||||
return code, { "search:"..search_name, "pattern:"..pattern_name };
|
||||
end;
|
||||
};
|
||||
scan_list = {
|
||||
global_code = [[local function scan_list(list, items) for item in pairs(items) do if list:contains(item) then return true; end end end]];
|
||||
}
|
||||
};
|
||||
|
||||
local function include_dep(dependency, code)
|
||||
local dep, dep_param = dependency:match("^([^:]+):?(.*)$");
|
||||
local dep_info = available_deps[dep];
|
||||
if not dep_info then
|
||||
module:log("error", "Dependency not found: %s", dep);
|
||||
return;
|
||||
end
|
||||
if code.included_deps[dependency] ~= nil then
|
||||
if code.included_deps[dependency] ~= true then
|
||||
module:log("error", "Circular dependency on %s", dep);
|
||||
end
|
||||
return;
|
||||
end
|
||||
code.included_deps[dependency] = false; -- Pending flag (used to detect circular references)
|
||||
for _, dep_dep in ipairs(dep_info.depends or {}) do
|
||||
include_dep(dep_dep, code);
|
||||
end
|
||||
if dep_info.global_code then
|
||||
if dep_param ~= "" then
|
||||
local global_code, deps = dep_info.global_code(dep_param);
|
||||
if deps then
|
||||
for _, dep_dep in ipairs(deps) do
|
||||
include_dep(dep_dep, code);
|
||||
end
|
||||
end
|
||||
table.insert(code.global_header, global_code);
|
||||
else
|
||||
table.insert(code.global_header, dep_info.global_code);
|
||||
end
|
||||
end
|
||||
if dep_info.local_code then
|
||||
if dep_param ~= "" then
|
||||
local local_code, deps = dep_info.local_code(dep_param);
|
||||
if deps then
|
||||
for _, dep_dep in ipairs(deps) do
|
||||
include_dep(dep_dep, code);
|
||||
end
|
||||
end
|
||||
table.insert(code, "\n\t\t-- "..dep.."\n\t\t"..local_code.."\n");
|
||||
else
|
||||
table.insert(code, "\n\t\t-- "..dep.."\n\t\t"..dep_info.local_code.."\n");
|
||||
end
|
||||
end
|
||||
code.included_deps[dependency] = true;
|
||||
end
|
||||
|
||||
local definition_handlers = module:require("definitions");
|
||||
local condition_handlers = module:require("conditions");
|
||||
local action_handlers = module:require("actions");
|
||||
|
||||
if module:get_option_boolean("firewall_experimental_user_marks", true) then
|
||||
module:require"marks";
|
||||
end
|
||||
|
||||
local function new_rule(ruleset, chain)
|
||||
assert(chain, "no chain specified");
|
||||
local rule = { conditions = {}, actions = {}, deps = {} };
|
||||
table.insert(ruleset[chain], rule);
|
||||
return rule;
|
||||
end
|
||||
|
||||
local function parse_firewall_rules(filename)
|
||||
local line_no = 0;
|
||||
|
||||
local function errmsg(err)
|
||||
return "Error compiling "..filename.." on line "..line_no..": "..err;
|
||||
end
|
||||
|
||||
local ruleset = {
|
||||
deliver = {};
|
||||
};
|
||||
|
||||
local chain = "deliver"; -- Default chain
|
||||
local rule;
|
||||
|
||||
local file, err = io.open(filename);
|
||||
if not file then return nil, err; end
|
||||
|
||||
local state; -- nil -> "rules" -> "actions" -> nil -> ...
|
||||
|
||||
local line_hold;
|
||||
for line in file:lines() do
|
||||
line = line:match("^%s*(.-)%s*$");
|
||||
if line_hold and line:sub(-1,-1) ~= "\\" then
|
||||
line = line_hold..line;
|
||||
line_hold = nil;
|
||||
elseif line:sub(-1,-1) == "\\" then
|
||||
line_hold = (line_hold or "")..line:sub(1,-2);
|
||||
end
|
||||
line_no = line_no + 1;
|
||||
|
||||
if line_hold or line:find("^[#;]") then -- luacheck: ignore 542
|
||||
-- No action; comment or partial line
|
||||
elseif line == "" then
|
||||
if state == "rules" then
|
||||
return nil, ("Expected an action on line %d for preceding criteria")
|
||||
:format(line_no);
|
||||
end
|
||||
state = nil;
|
||||
elseif not(state) and line:sub(1, 2) == "::" then
|
||||
chain = line:gsub("^::%s*", "");
|
||||
local chain_info = chains[chain];
|
||||
if not chain_info then
|
||||
if chain:match("^user/") then
|
||||
chains[chain] = { type = "event", priority = 1, pass_return = false };
|
||||
else
|
||||
return nil, errmsg("Unknown chain: "..chain);
|
||||
end
|
||||
elseif chain_info.type ~= "event" then
|
||||
return nil, errmsg("Only event chains supported at the moment");
|
||||
end
|
||||
ruleset[chain] = ruleset[chain] or {};
|
||||
elseif not(state) and line:sub(1,1) == "%" then -- Definition (zone, limit, etc.)
|
||||
local what, name = line:match("^%%%s*([%w_]+) +([^ :]+)");
|
||||
if not definition_handlers[what] then
|
||||
return nil, errmsg("Definition of unknown object: "..what);
|
||||
elseif not name or not idsafe(name) then
|
||||
return nil, errmsg("Invalid "..what.." name");
|
||||
end
|
||||
|
||||
local val = line:match(": ?(.*)$");
|
||||
if not val and line:find(":<") then -- Read from file
|
||||
local fn = line:match(":< ?(.-)%s*$");
|
||||
if not fn then
|
||||
return nil, errmsg("Unable to parse filename");
|
||||
end
|
||||
local f, err = io.open(fn);
|
||||
if not f then return nil, errmsg(err); end
|
||||
val = f:read("*a"):gsub("\r?\n", " "):gsub("%s+$", "");
|
||||
end
|
||||
if not val then
|
||||
return nil, errmsg("No value given for definition");
|
||||
end
|
||||
val = stripslashes(val);
|
||||
local ok, ret = pcall(definition_handlers[what], name, val);
|
||||
if not ok then
|
||||
return nil, errmsg(ret);
|
||||
end
|
||||
|
||||
if not active_definitions[what] then
|
||||
active_definitions[what] = {};
|
||||
end
|
||||
active_definitions[what][name] = ret;
|
||||
elseif line:find("^[%w_ ]+[%.=]") then
|
||||
-- Action
|
||||
if state == nil then
|
||||
-- This is a standalone action with no conditions
|
||||
rule = new_rule(ruleset, chain);
|
||||
end
|
||||
state = "actions";
|
||||
-- Action handlers?
|
||||
local action = line:match("^[%w_ ]+"):upper():gsub(" ", "_");
|
||||
if not action_handlers[action] then
|
||||
return nil, ("Unknown action on line %d: %s"):format(line_no, action or "<unknown>");
|
||||
end
|
||||
table.insert(rule.actions, "-- "..line)
|
||||
local ok, action_string, action_deps = pcall(action_handlers[action], line:match("=(.+)$"));
|
||||
if not ok then
|
||||
return nil, errmsg(action_string);
|
||||
end
|
||||
table.insert(rule.actions, action_string);
|
||||
for _, dep in ipairs(action_deps or {}) do
|
||||
table.insert(rule.deps, dep);
|
||||
end
|
||||
elseif state == "actions" then -- state is actions but action pattern did not match
|
||||
state = nil; -- Awaiting next rule, etc.
|
||||
table.insert(ruleset[chain], rule);
|
||||
rule = nil;
|
||||
else
|
||||
if not state then
|
||||
state = "rules";
|
||||
rule = new_rule(ruleset, chain);
|
||||
end
|
||||
-- Check standard modifiers for the condition (e.g. NOT)
|
||||
local negated;
|
||||
local condition = line:match("^[^:=%.?]*");
|
||||
if condition:find("%f[%w]NOT%f[^%w]") then
|
||||
local s, e = condition:match("%f[%w]()NOT()%f[^%w]");
|
||||
condition = (condition:sub(1,s-1)..condition:sub(e+1, -1)):match("^%s*(.-)%s*$");
|
||||
negated = true;
|
||||
end
|
||||
condition = condition:gsub(" ", "_");
|
||||
if not condition_handlers[condition] then
|
||||
return nil, ("Unknown condition on line %d: %s"):format(line_no, (condition:gsub("_", " ")));
|
||||
end
|
||||
-- Get the code for this condition
|
||||
local ok, condition_code, condition_deps = pcall(condition_handlers[condition], line:match(":%s?(.+)$"));
|
||||
if not ok then
|
||||
return nil, errmsg(condition_code);
|
||||
end
|
||||
if negated then condition_code = "not("..condition_code..")"; end
|
||||
table.insert(rule.conditions, condition_code);
|
||||
for _, dep in ipairs(condition_deps or {}) do
|
||||
table.insert(rule.deps, dep);
|
||||
end
|
||||
end
|
||||
end
|
||||
return ruleset;
|
||||
end
|
||||
|
||||
local function process_firewall_rules(ruleset)
|
||||
-- Compile ruleset and return complete code
|
||||
|
||||
local chain_handlers = {};
|
||||
|
||||
-- Loop through the chains in the parsed ruleset (e.g. incoming, outgoing)
|
||||
for chain_name, rules in pairs(ruleset) do
|
||||
local code = { included_deps = {}, global_header = {} };
|
||||
local condition_uses = {};
|
||||
-- This inner loop assumes chain is an event-based, not a filter-based
|
||||
-- chain (filter-based will be added later)
|
||||
for _, rule in ipairs(rules) do
|
||||
for _, condition in ipairs(rule.conditions) do
|
||||
if condition:find("^not%(.+%)$") then
|
||||
condition = condition:match("^not%((.+)%)$");
|
||||
end
|
||||
condition_uses[condition] = (condition_uses[condition] or 0) + 1;
|
||||
end
|
||||
end
|
||||
|
||||
local condition_cache, n_conditions = {}, 0;
|
||||
for _, rule in ipairs(rules) do
|
||||
for _, dep in ipairs(rule.deps) do
|
||||
include_dep(dep, code);
|
||||
end
|
||||
table.insert(code, "\n\t\t");
|
||||
local rule_code;
|
||||
if #rule.conditions > 0 then
|
||||
for i, condition in ipairs(rule.conditions) do
|
||||
local negated = condition:match("^not%(.+%)$");
|
||||
if negated then
|
||||
condition = condition:match("^not%((.+)%)$");
|
||||
end
|
||||
if condition_uses[condition] > 1 then
|
||||
local name = condition_cache[condition];
|
||||
if not name then
|
||||
n_conditions = n_conditions + 1;
|
||||
name = "condition"..n_conditions;
|
||||
condition_cache[condition] = name;
|
||||
table.insert(code, "local "..name.." = "..condition..";\n\t\t");
|
||||
end
|
||||
rule.conditions[i] = (negated and "not(" or "")..name..(negated and ")" or "");
|
||||
else
|
||||
rule.conditions[i] = (negated and "not(" or "(")..condition..")";
|
||||
end
|
||||
end
|
||||
|
||||
rule_code = "if "..table.concat(rule.conditions, " and ").." then\n\t\t\t"
|
||||
..table.concat(rule.actions, "\n\t\t\t")
|
||||
.."\n\t\tend\n";
|
||||
else
|
||||
rule_code = table.concat(rule.actions, "\n\t\t");
|
||||
end
|
||||
table.insert(code, rule_code);
|
||||
end
|
||||
|
||||
for name in pairs(definition_handlers) do
|
||||
table.insert(code.global_header, 1, "local "..name:lower().."s = definitions."..name..";");
|
||||
end
|
||||
|
||||
local code_string = "return function (definitions, fire_event, log, module, pass_return)\n\t"
|
||||
..table.concat(code.global_header, "\n\t")
|
||||
.."\n\tlocal db = require 'util.debug';\n\n\t"
|
||||
.."return function (event)\n\t\t"
|
||||
.."local stanza, session = event.stanza, event.origin;\n"
|
||||
..table.concat(code, "")
|
||||
.."\n\tend;\nend";
|
||||
|
||||
chain_handlers[chain_name] = code_string;
|
||||
end
|
||||
|
||||
return chain_handlers;
|
||||
end
|
||||
|
||||
local function compile_firewall_rules(filename)
|
||||
local ruleset, err = parse_firewall_rules(filename);
|
||||
if not ruleset then return nil, err; end
|
||||
local chain_handlers = process_firewall_rules(ruleset);
|
||||
return chain_handlers;
|
||||
end
|
||||
|
||||
-- Compile handler code into a factory that produces a valid event handler. Factory accepts
|
||||
-- a value to be returned on PASS
|
||||
local function compile_handler(code_string, filename)
|
||||
-- Prepare event handler function
|
||||
local chunk, err = envload(code_string, "="..filename, _G);
|
||||
if not chunk then
|
||||
return nil, "Error compiling (probably a compiler bug, please report): "..err;
|
||||
end
|
||||
local function fire_event(name, data)
|
||||
return module:fire_event(name, data);
|
||||
end
|
||||
local init_ok, initialized_chunk = pcall(chunk);
|
||||
if not init_ok then
|
||||
return nil, "Error initializing compiled rules: "..initialized_chunk;
|
||||
end
|
||||
return function (pass_return)
|
||||
return initialized_chunk(active_definitions, fire_event, logger(filename), module, pass_return); -- Returns event handler with upvalues
|
||||
end
|
||||
end
|
||||
|
||||
local function resolve_script_path(script_path)
|
||||
local relative_to = prosody.paths.config;
|
||||
if script_path:match("^module:") then
|
||||
relative_to = module.path:sub(1, -#("/mod_"..module.name..".lua"));
|
||||
script_path = script_path:match("^module:(.+)$");
|
||||
end
|
||||
return resolve_relative_path(relative_to, script_path);
|
||||
end
|
||||
|
||||
-- [filename] = { last_modified = ..., events_hooked = { [name] = handler } }
|
||||
local loaded_scripts = {};
|
||||
|
||||
function load_script(script)
|
||||
script = resolve_script_path(script);
|
||||
local last_modified = (lfs.attributes(script) or {}).modification or os.time();
|
||||
if loaded_scripts[script] then
|
||||
if loaded_scripts[script].last_modified == last_modified then
|
||||
return; -- Already loaded, and source file hasn't changed
|
||||
end
|
||||
module:log("debug", "Reloading %s", script);
|
||||
-- Already loaded, but the source file has changed
|
||||
-- unload it now, and we'll load the new version below
|
||||
unload_script(script, true);
|
||||
end
|
||||
local chain_functions, err = compile_firewall_rules(script);
|
||||
|
||||
if not chain_functions then
|
||||
module:log("error", "Error compiling %s: %s", script, err or "unknown error");
|
||||
return;
|
||||
end
|
||||
|
||||
-- Loop through the chains in the script, and for each chain attach the compiled code to the
|
||||
-- relevant events, keeping track in events_hooked so we can cleanly unload later
|
||||
local events_hooked = {};
|
||||
for chain, handler_code in pairs(chain_functions) do
|
||||
local new_handler, err = compile_handler(handler_code, "mod_firewall::"..chain);
|
||||
if not new_handler then
|
||||
module:log("error", "Compilation error for %s: %s", script, err);
|
||||
else
|
||||
local chain_definition = chains[chain];
|
||||
if chain_definition and chain_definition.type == "event" then
|
||||
local handler = new_handler(chain_definition.pass_return);
|
||||
for _, event_name in ipairs(chain_definition) do
|
||||
events_hooked[event_name] = handler;
|
||||
module:hook(event_name, handler, chain_definition.priority);
|
||||
end
|
||||
elseif not chain:sub(1, 5) == "user/" then
|
||||
module:log("warn", "Unknown chain %q", chain);
|
||||
end
|
||||
local event_name, handler = "firewall/chains/"..chain, new_handler(false);
|
||||
events_hooked[event_name] = handler;
|
||||
module:hook(event_name, handler);
|
||||
end
|
||||
end
|
||||
loaded_scripts[script] = { last_modified = last_modified, events_hooked = events_hooked };
|
||||
module:log("debug", "Loaded %s", script);
|
||||
end
|
||||
|
||||
--COMPAT w/0.9 (no module:unhook()!)
|
||||
local function module_unhook(event, handler)
|
||||
return module:unhook_object_event((hosts[module.host] or prosody).events, event, handler);
|
||||
end
|
||||
|
||||
function unload_script(script, is_reload)
|
||||
script = resolve_script_path(script);
|
||||
local script_info = loaded_scripts[script];
|
||||
if not script_info then
|
||||
return; -- Script not loaded
|
||||
end
|
||||
local events_hooked = script_info.events_hooked;
|
||||
for event_name, event_handler in pairs(events_hooked) do
|
||||
module_unhook(event_name, event_handler);
|
||||
events_hooked[event_name] = nil;
|
||||
end
|
||||
loaded_scripts[script] = nil;
|
||||
if not is_reload then
|
||||
module:log("debug", "Unloaded %s", script);
|
||||
end
|
||||
end
|
||||
|
||||
-- Given a set of scripts (e.g. from config) figure out which ones need to
|
||||
-- be loaded, which are already loaded but need unloading, and which to reload
|
||||
function load_unload_scripts(script_list)
|
||||
local wanted_scripts = script_list / resolve_script_path;
|
||||
local currently_loaded = set.new(it.to_array(it.keys(loaded_scripts)));
|
||||
local scripts_to_unload = currently_loaded - wanted_scripts;
|
||||
for script in wanted_scripts do
|
||||
-- If the script is already loaded, this is fine - it will
|
||||
-- reload the script for us if the file has changed
|
||||
load_script(script);
|
||||
end
|
||||
for script in scripts_to_unload do
|
||||
unload_script(script);
|
||||
end
|
||||
end
|
||||
|
||||
function module.load()
|
||||
if not prosody.arg then return end -- Don't run in prosodyctl
|
||||
local firewall_scripts = module:get_option_set("firewall_scripts", {});
|
||||
load_unload_scripts(firewall_scripts);
|
||||
-- Replace contents of definitions table (shared) with active definitions
|
||||
for k in it.keys(definitions) do definitions[k] = nil; end
|
||||
for k,v in pairs(active_definitions) do definitions[k] = v; end
|
||||
end
|
||||
|
||||
function module.save()
|
||||
return { active_definitions = active_definitions, loaded_scripts = loaded_scripts };
|
||||
end
|
||||
|
||||
function module.restore(state)
|
||||
active_definitions = state.active_definitions;
|
||||
loaded_scripts = state.loaded_scripts;
|
||||
end
|
||||
|
||||
module:hook_global("config-reloaded", function ()
|
||||
load_unload_scripts(module:get_option_set("firewall_scripts", {}));
|
||||
end);
|
||||
|
||||
function module.command(arg)
|
||||
if not arg[1] or arg[1] == "--help" then
|
||||
require"util.prosodyctl".show_usage([[mod_firewall <firewall.pfw>]], [[Compile files with firewall rules to Lua code]]);
|
||||
return 1;
|
||||
end
|
||||
local verbose = arg[1] == "-v";
|
||||
if verbose then table.remove(arg, 1); end
|
||||
|
||||
if arg[1] == "test" then
|
||||
table.remove(arg, 1);
|
||||
return module:require("test")(arg);
|
||||
end
|
||||
|
||||
local serialize = require "util.serialization".serialize;
|
||||
if verbose then
|
||||
print("local logger = require \"util.logger\".init;");
|
||||
print();
|
||||
print("local function fire_event(name, data)\n\tmodule:fire_event(name, data)\nend");
|
||||
print();
|
||||
end
|
||||
|
||||
for _, filename in ipairs(arg) do
|
||||
filename = resolve_script_path(filename);
|
||||
print("do -- File "..filename);
|
||||
local chain_functions = assert(compile_firewall_rules(filename));
|
||||
if verbose then
|
||||
print();
|
||||
print("local active_definitions = "..serialize(active_definitions)..";");
|
||||
print();
|
||||
end
|
||||
local c = 0;
|
||||
for chain, handler_code in pairs(chain_functions) do
|
||||
c = c + 1;
|
||||
print("---- Chain "..chain:gsub("_", " "));
|
||||
local chain_func_name = "chain_"..tostring(c).."_"..chain:gsub("%p", "_");
|
||||
if not verbose then
|
||||
print(("%s = %s;"):format(chain_func_name, handler_code:sub(8)));
|
||||
else
|
||||
|
||||
print(("local %s = (%s)(active_definitions, fire_event, logger(%q));"):format(chain_func_name, handler_code:sub(8), filename));
|
||||
print();
|
||||
|
||||
local chain_definition = chains[chain];
|
||||
if chain_definition and chain_definition.type == "event" then
|
||||
for _, event_name in ipairs(chain_definition) do
|
||||
print(("module:hook(%q, %s, %d);"):format(event_name, chain_func_name, chain_definition.priority or 0));
|
||||
end
|
||||
end
|
||||
print(("module:hook(%q, %s, %d);"):format("firewall/chains/"..chain, chain_func_name, chain_definition.priority or 0));
|
||||
end
|
||||
|
||||
print("---- End of chain "..chain);
|
||||
print();
|
||||
end
|
||||
print("end -- End of file "..filename);
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- Console
|
||||
|
||||
local console_env = module:shared("/*/admin_shell/env");
|
||||
|
||||
console_env.firewall = {};
|
||||
|
||||
function console_env.firewall:mark(user_jid, mark_name)
|
||||
local username, host = jid.split(user_jid);
|
||||
if not username or not hosts[host] then
|
||||
return nil, "Invalid JID supplied";
|
||||
elseif not idsafe(mark_name) then
|
||||
return nil, "Invalid characters in mark name";
|
||||
end
|
||||
if not module:context(host):fire_event("firewall/marked/user", {
|
||||
username = session.username;
|
||||
mark = mark_name;
|
||||
timestamp = os.time();
|
||||
}) then
|
||||
return nil, "Mark not set - is mod_firewall loaded on that host?";
|
||||
end
|
||||
return true, "User marked";
|
||||
end
|
||||
|
||||
function console_env.firewall:unmark(jid, mark_name)
|
||||
local username, host = jid.split(user_jid);
|
||||
if not username or not hosts[host] then
|
||||
return nil, "Invalid JID supplied";
|
||||
elseif not idsafe(mark_name) then
|
||||
return nil, "Invalid characters in mark name";
|
||||
end
|
||||
if not module:context(host):fire_event("firewall/unmarked/user", {
|
||||
username = session.username;
|
||||
mark = mark_name;
|
||||
}) then
|
||||
return nil, "Mark not removed - is mod_firewall loaded on that host?";
|
||||
end
|
||||
return true, "User unmarked";
|
||||
end
|
75
roles/jitsi/files/prosody/modules/mod_firewall/test.lib.lua
Normal file
75
roles/jitsi/files/prosody/modules/mod_firewall/test.lib.lua
Normal file
@ -0,0 +1,75 @@
|
||||
-- luacheck: globals load_unload_scripts
|
||||
local set = require "util.set";
|
||||
local ltn12 = require "ltn12";
|
||||
|
||||
local xmppstream = require "util.xmppstream";
|
||||
|
||||
local function stderr(...)
|
||||
io.stderr:write("** ", table.concat({...}, "\t", 1, select("#", ...)), "\n");
|
||||
end
|
||||
|
||||
return function (arg)
|
||||
require "net.http".request = function (url, ex, cb)
|
||||
stderr("Making HTTP request to "..url);
|
||||
local body_table = {};
|
||||
local ok, response_status, response_headers = require "ssl.https".request({
|
||||
url = url;
|
||||
headers = ex.headers;
|
||||
method = ex.body and "POST" or "GET";
|
||||
sink = ltn12.sink.table(body_table);
|
||||
source = ex.body and ltn12.source.string(ex.body) or nil;
|
||||
});
|
||||
stderr("HTTP response "..response_status);
|
||||
cb(table.concat(body_table), response_status, { headers = response_headers });
|
||||
return true;
|
||||
end;
|
||||
|
||||
local stats_dropped, stats_passed = 0, 0;
|
||||
|
||||
load_unload_scripts(set.new(arg));
|
||||
local stream_callbacks = { default_ns = "jabber:client" };
|
||||
|
||||
function stream_callbacks.streamopened(session)
|
||||
session.notopen = nil;
|
||||
end
|
||||
function stream_callbacks.streamclosed()
|
||||
end
|
||||
function stream_callbacks.error(session, error_name, error_message) -- luacheck: ignore 212/session
|
||||
stderr("Fatal error parsing XML stream: "..error_name..": "..tostring(error_message))
|
||||
assert(false);
|
||||
end
|
||||
function stream_callbacks.handlestanza(session, stanza)
|
||||
if not module:fire_event("firewall/chains/deliver", { origin = session, stanza = stanza }) then
|
||||
stats_passed = stats_passed + 1;
|
||||
print(stanza);
|
||||
print("");
|
||||
else
|
||||
stats_dropped = stats_dropped + 1;
|
||||
end
|
||||
end
|
||||
|
||||
local session = { notopen = true };
|
||||
function session.send(stanza)
|
||||
stderr("Reply:", "\n"..tostring(stanza).."\n");
|
||||
end
|
||||
local stream = xmppstream.new(session, stream_callbacks);
|
||||
stream:feed("<stream:stream xmlns:stream='http://etherx.jabber.org/streams' xmlns='jabber:client'>");
|
||||
local line_count = 0;
|
||||
for line in io.lines() do
|
||||
line_count = line_count + 1;
|
||||
local ok, err = stream:feed(line.."\n");
|
||||
if not ok then
|
||||
stderr("Fatal XML parse error on line "..line_count..": "..err);
|
||||
return 1;
|
||||
end
|
||||
end
|
||||
|
||||
stderr("Summary");
|
||||
stderr("-------");
|
||||
stderr("");
|
||||
stderr(stats_dropped + stats_passed, "processed");
|
||||
stderr(stats_passed, "passed");
|
||||
stderr(stats_dropped, "dropped");
|
||||
stderr(line_count, "input lines");
|
||||
stderr("");
|
||||
end
|
623
roles/jitsi/files/prosody/modules/mod_fmuc.lua
Normal file
623
roles/jitsi/files/prosody/modules/mod_fmuc.lua
Normal file
@ -0,0 +1,623 @@
|
||||
--- activate under main muc component
|
||||
--- Add the following config under the main muc component
|
||||
--- muc_room_default_presence_broadcast = {
|
||||
--- visitor = false;
|
||||
--- participant = true;
|
||||
--- moderator = true;
|
||||
--- };
|
||||
--- Enable in global modules: 's2s_bidi'
|
||||
--- Make sure 's2s' is not in modules_disabled
|
||||
--- NOTE: Make sure all communication between prosodies is using the real jids ([foo]room1@muc.example.com), as there
|
||||
--- are certain configs for whitelisted domains and connections that are domain based
|
||||
--- TODO: filter presence from main occupants back to main prosody
|
||||
local jid = require 'util.jid';
|
||||
local st = require 'util.stanza';
|
||||
local new_id = require 'util.id'.medium;
|
||||
local filters = require 'util.filters';
|
||||
|
||||
local util = module:require 'util';
|
||||
local ends_with = util.ends_with;
|
||||
local is_vpaas = util.is_vpaas;
|
||||
local room_jid_match_rewrite = util.room_jid_match_rewrite;
|
||||
local get_room_from_jid = util.get_room_from_jid;
|
||||
local get_focus_occupant = util.get_focus_occupant;
|
||||
local internal_room_jid_match_rewrite = util.internal_room_jid_match_rewrite;
|
||||
local presence_check_status = util.presence_check_status;
|
||||
|
||||
-- this is the main virtual host of this vnode
|
||||
local local_domain = module:get_option_string('muc_mapper_domain_base');
|
||||
if not local_domain then
|
||||
module:log('warn', "No 'muc_mapper_domain_base' option set, disabling fmuc plugin");
|
||||
return;
|
||||
end
|
||||
|
||||
-- this is the main virtual host of the main prosody that this vnode serves
|
||||
local main_domain = module:get_option_string('main_domain');
|
||||
if not main_domain then
|
||||
module:log('warn', "No 'main_domain' option set, disabling fmuc plugin");
|
||||
return;
|
||||
end
|
||||
|
||||
local muc_domain_prefix = module:get_option_string('muc_mapper_domain_prefix', 'conference');
|
||||
local local_muc_domain = muc_domain_prefix..'.'..local_domain;
|
||||
|
||||
local NICK_NS = 'http://jabber.org/protocol/nick';
|
||||
|
||||
-- we send stats for the total number of rooms, total number of participants and total number of visitors
|
||||
local measure_rooms = module:measure('vnode-rooms', 'amount');
|
||||
local measure_participants = module:measure('vnode-participants', 'amount');
|
||||
local measure_visitors = module:measure('vnode-visitors', 'amount');
|
||||
|
||||
local sent_iq_cache = require 'util.cache'.new(200);
|
||||
|
||||
local sessions = prosody.full_sessions;
|
||||
|
||||
local um_is_admin = require 'core.usermanager'.is_admin;
|
||||
local function is_admin(jid)
|
||||
return um_is_admin(jid, module.host);
|
||||
end
|
||||
|
||||
-- mark all occupants as visitors
|
||||
module:hook('muc-occupant-pre-join', function (event)
|
||||
local occupant, room, origin, stanza = event.occupant, event.room, event.origin, event.stanza;
|
||||
local node, host = jid.split(occupant.bare_jid);
|
||||
|
||||
if prosody.hosts[host] and not is_admin(occupant.bare_jid) then
|
||||
if room._main_room_lobby_enabled then
|
||||
origin.send(st.error_reply(stanza, 'cancel', 'not-allowed', 'Visitors not allowed while lobby is on!')
|
||||
:tag('no-visitors-lobby', { xmlns = 'jitsi:visitors' }));
|
||||
return true;
|
||||
else
|
||||
occupant.role = 'visitor';
|
||||
end
|
||||
end
|
||||
end, 3);
|
||||
|
||||
-- if a visitor leaves we want to lower its hand if it was still raised before leaving
|
||||
-- this is to clear indication for promotion on moderators visitors list
|
||||
module:hook('muc-occupant-pre-leave', function (event)
|
||||
local occupant = event.occupant;
|
||||
|
||||
---- we are interested only of visitors presence
|
||||
if occupant.role ~= 'visitor' then
|
||||
return;
|
||||
end
|
||||
|
||||
local room = event.room;
|
||||
|
||||
-- let's check if the visitor has a raised hand send a lower hand
|
||||
-- to main prosody
|
||||
local pr = occupant:get_presence();
|
||||
|
||||
local raiseHand = pr:get_child_text('jitsi_participant_raisedHand');
|
||||
|
||||
-- a promotion detected let's send it to main prosody
|
||||
if raiseHand and #raiseHand > 0 then
|
||||
local iq_id = new_id();
|
||||
sent_iq_cache:set(iq_id, socket.gettime());
|
||||
local promotion_request = st.iq({
|
||||
type = 'set',
|
||||
to = 'visitors.'..main_domain,
|
||||
from = local_domain,
|
||||
id = iq_id })
|
||||
:tag('visitors', { xmlns = 'jitsi:visitors',
|
||||
room = jid.join(jid.node(room.jid), muc_domain_prefix..'.'..main_domain) })
|
||||
:tag('promotion-request', {
|
||||
xmlns = 'jitsi:visitors',
|
||||
jid = occupant.jid,
|
||||
time = nil;
|
||||
}):up();
|
||||
|
||||
module:send(promotion_request);
|
||||
end
|
||||
|
||||
end, 1); -- rate limit is 0
|
||||
|
||||
-- Returns the main participants count and the visitors count
|
||||
local function get_occupant_counts(room)
|
||||
local main_count = 0;
|
||||
local visitors_count = 0;
|
||||
|
||||
for _, o in room:each_occupant() do
|
||||
if o.role == 'visitor' then
|
||||
visitors_count = visitors_count + 1;
|
||||
elseif not is_admin(o.bare_jid) then
|
||||
main_count = main_count + 1;
|
||||
end
|
||||
end
|
||||
|
||||
return main_count, visitors_count;
|
||||
end
|
||||
|
||||
local function cancel_destroy_timer(room)
|
||||
if room.visitors_destroy_timer then
|
||||
room.visitors_destroy_timer:stop();
|
||||
room.visitors_destroy_timer = nil;
|
||||
end
|
||||
end
|
||||
|
||||
-- schedules a new destroy timer which will destroy the room if there are no visitors after the timeout
|
||||
local function schedule_destroy_timer(room)
|
||||
cancel_destroy_timer(room);
|
||||
|
||||
room.visitors_destroy_timer = module:add_timer(15, function()
|
||||
-- if the room is being destroyed, ignore
|
||||
if room.destroying then
|
||||
return;
|
||||
end
|
||||
|
||||
local main_count, visitors_count = get_occupant_counts(room);
|
||||
|
||||
if visitors_count == 0 then
|
||||
module:log('info', 'Will destroy:%s main_occupants:%s visitors:%s', room.jid, main_count, visitors_count);
|
||||
room:destroy(nil, 'No visitors.');
|
||||
end
|
||||
end);
|
||||
end
|
||||
|
||||
-- when occupant is leaving forward presences to jicofo for visitors
|
||||
-- do not check occupant.role as it maybe already reset
|
||||
-- if there are no main occupants or no visitors, destroy the room (give 15 seconds of grace period for reconnections)
|
||||
module:hook('muc-occupant-left', function (event)
|
||||
local room, occupant = event.room, event.occupant;
|
||||
local occupant_domain = jid.host(occupant.bare_jid);
|
||||
|
||||
if prosody.hosts[occupant_domain] and not is_admin(occupant.bare_jid) then
|
||||
local focus_occupant = get_focus_occupant(room);
|
||||
if not focus_occupant then
|
||||
module:log('info', 'No focus found for %s', room.jid);
|
||||
return;
|
||||
end
|
||||
-- Let's forward unavailable presence to the special jicofo
|
||||
room:route_stanza(st.presence({
|
||||
to = focus_occupant.jid,
|
||||
from = internal_room_jid_match_rewrite(occupant.nick),
|
||||
type = 'unavailable' })
|
||||
:tag('x', { xmlns = 'http://jabber.org/protocol/muc#user' })
|
||||
:tag('item', {
|
||||
affiliation = room:get_affiliation(occupant.bare_jid) or 'none';
|
||||
role = 'none';
|
||||
nick = event.nick;
|
||||
jid = occupant.bare_jid }):up():up());
|
||||
end
|
||||
|
||||
-- if the room is being destroyed, ignore
|
||||
if room.destroying then
|
||||
return;
|
||||
end
|
||||
|
||||
-- if there are no main participants, the main room will be destroyed and
|
||||
-- we can destroy and the visitor one as when jicofo leaves all visitors will reload
|
||||
-- if there are no visitors give them 15 secs to reconnect, if not destroy it
|
||||
local main_count, visitors_count = get_occupant_counts(room);
|
||||
|
||||
if visitors_count == 0 then
|
||||
schedule_destroy_timer(room);
|
||||
end
|
||||
end);
|
||||
|
||||
-- forward visitor presences to jicofo
|
||||
-- detects raise hand in visitors presence, this is request for promotion
|
||||
module:hook('muc-broadcast-presence', function (event)
|
||||
local occupant = event.occupant;
|
||||
|
||||
---- we are interested only of visitors presence to send it to jicofo
|
||||
if occupant.role ~= 'visitor' then
|
||||
return;
|
||||
end
|
||||
|
||||
local room = event.room;
|
||||
local focus_occupant = get_focus_occupant(room);
|
||||
|
||||
if not focus_occupant then
|
||||
return;
|
||||
end
|
||||
|
||||
local actor, base_presence, nick, reason, x = event.actor, event.stanza, event.nick, event.reason, event.x;
|
||||
local actor_nick;
|
||||
if actor then
|
||||
actor_nick = jid.resource(room:get_occupant_jid(actor));
|
||||
end
|
||||
|
||||
-- create a presence to send it to jicofo, as jicofo is special :)
|
||||
local full_x = st.clone(x.full or x);
|
||||
|
||||
room:build_item_list(occupant, full_x, false, nick, actor_nick, actor, reason);
|
||||
local full_p = st.clone(base_presence):add_child(full_x);
|
||||
full_p.attr.to = focus_occupant.jid;
|
||||
room:route_to_occupant(focus_occupant, full_p);
|
||||
|
||||
local raiseHand = full_p:get_child_text('jitsi_participant_raisedHand');
|
||||
-- a promotion detected let's send it to main prosody
|
||||
if raiseHand then
|
||||
local user_id;
|
||||
local is_moderator;
|
||||
local session = sessions[occupant.jid];
|
||||
local identity = session and session.jitsi_meet_context_user;
|
||||
|
||||
if is_vpaas(room) and identity then
|
||||
-- in case of moderator in vpaas meeting we want to do auto-promotion
|
||||
local is_vpaas_moderator = identity.moderator;
|
||||
if is_vpaas_moderator == 'true' or is_vpaas_moderator == true then
|
||||
is_moderator = true;
|
||||
end
|
||||
else
|
||||
-- The case with single moderator in the room, we want to report our id
|
||||
-- so we can be auto promoted
|
||||
if identity and identity.id then
|
||||
user_id = session.jitsi_meet_context_user.id;
|
||||
|
||||
if room._data.moderator_id then
|
||||
if room._data.moderator_id == user_id then
|
||||
is_moderator = true;
|
||||
end
|
||||
elseif session.auth_token then
|
||||
-- non-vpass and having a token is considered a moderator
|
||||
is_moderator = true;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local iq_id = new_id();
|
||||
sent_iq_cache:set(iq_id, socket.gettime());
|
||||
local promotion_request = st.iq({
|
||||
type = 'set',
|
||||
to = 'visitors.'..main_domain,
|
||||
from = local_domain,
|
||||
id = iq_id })
|
||||
:tag('visitors', { xmlns = 'jitsi:visitors',
|
||||
room = jid.join(jid.node(room.jid), muc_domain_prefix..'.'..main_domain) })
|
||||
:tag('promotion-request', {
|
||||
xmlns = 'jitsi:visitors',
|
||||
jid = occupant.jid,
|
||||
time = raiseHand,
|
||||
userId = user_id,
|
||||
forcePromote = is_moderator and 'true' or 'false';
|
||||
}):up();
|
||||
|
||||
local nick_element = occupant:get_presence():get_child('nick', NICK_NS);
|
||||
if nick_element then
|
||||
promotion_request:add_child(nick_element);
|
||||
end
|
||||
|
||||
module:send(promotion_request);
|
||||
end
|
||||
|
||||
return;
|
||||
end);
|
||||
|
||||
-- listens for responses to the iq sent for requesting promotion and forward it to the visitor
|
||||
local function stanza_handler(event)
|
||||
local origin, stanza = event.origin, event.stanza;
|
||||
|
||||
if stanza.name ~= 'iq' then
|
||||
return;
|
||||
end
|
||||
|
||||
if stanza.attr.type == 'result' and sent_iq_cache:get(stanza.attr.id) then
|
||||
sent_iq_cache:set(stanza.attr.id, nil);
|
||||
return true;
|
||||
end
|
||||
|
||||
if stanza.attr.type ~= 'set' then
|
||||
return;
|
||||
end
|
||||
|
||||
local visitors_iq = event.stanza:get_child('visitors', 'jitsi:visitors');
|
||||
if not visitors_iq then
|
||||
return;
|
||||
end
|
||||
|
||||
if stanza.attr.from ~= 'visitors.'..main_domain then
|
||||
module:log('warn', 'not from visitors component, ignore! %s', stanza);
|
||||
return true;
|
||||
end
|
||||
|
||||
local room_jid = visitors_iq.attr.room;
|
||||
local room = get_room_from_jid(room_jid_match_rewrite(room_jid));
|
||||
|
||||
if not room then
|
||||
module:log('warn', 'No room found %s', room_jid);
|
||||
return;
|
||||
end
|
||||
|
||||
local request_promotion = visitors_iq:get_child('promotion-response');
|
||||
if not request_promotion then
|
||||
return;
|
||||
end
|
||||
|
||||
-- respond with successful receiving the iq
|
||||
origin.send(st.iq({
|
||||
type = 'result';
|
||||
from = stanza.attr.to;
|
||||
to = stanza.attr.from;
|
||||
id = stanza.attr.id
|
||||
}));
|
||||
|
||||
local req_jid = request_promotion.attr.jid;
|
||||
-- now let's find the occupant and forward the response
|
||||
local occupant = room:get_occupant_by_real_jid(req_jid);
|
||||
|
||||
if occupant then
|
||||
stanza.attr.to = occupant.jid;
|
||||
stanza.attr.from = room.jid;
|
||||
room:route_to_occupant(occupant, stanza);
|
||||
return true;
|
||||
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
|
||||
-- if the message received ends with the main domain, these are system messages
|
||||
-- for visitors, let's correct the room name there
|
||||
local function message_handler(event)
|
||||
local origin, stanza = event.origin, event.stanza;
|
||||
|
||||
if ends_with(stanza.attr.from, main_domain) then
|
||||
stanza.attr.from = stanza.attr.from:sub(1, -(main_domain:len() + 1))..local_domain;
|
||||
end
|
||||
end
|
||||
|
||||
process_host_module(local_domain, function(host_module, host)
|
||||
host_module:hook('iq/host', stanza_handler, 10);
|
||||
host_module:hook('message/full', message_handler);
|
||||
end);
|
||||
|
||||
-- only live chat is supported for visitors
|
||||
module:hook('muc-occupant-groupchat', function(event)
|
||||
local occupant, room, stanza = event.occupant, event.room, event.stanza;
|
||||
local from = stanza.attr.from;
|
||||
local occupant_host;
|
||||
|
||||
-- if there is no occupant this is a message from main, probably coming from other vnode
|
||||
if occupant then
|
||||
occupant_host = jid.host(occupant.bare_jid);
|
||||
|
||||
-- we manage nick only for visitors
|
||||
if occupant_host ~= main_domain then
|
||||
-- add to message stanza display name for the visitor
|
||||
-- remove existing nick to avoid forgery
|
||||
stanza:remove_children('nick', NICK_NS);
|
||||
local nick_element = occupant:get_presence():get_child('nick', NICK_NS);
|
||||
if nick_element then
|
||||
stanza:add_child(nick_element);
|
||||
else
|
||||
stanza:tag('nick', { xmlns = NICK_NS })
|
||||
:text('anonymous'):up();
|
||||
end
|
||||
end
|
||||
|
||||
stanza.attr.from = occupant.nick;
|
||||
else
|
||||
stanza.attr.from = jid.join(jid.node(from), module.host);
|
||||
end
|
||||
|
||||
-- let's send it to main chat and rest of visitors here
|
||||
for _, o in room:each_occupant() do
|
||||
-- filter remote occupants
|
||||
if jid.host(o.bare_jid) == local_domain then
|
||||
room:route_to_occupant(o, stanza)
|
||||
end
|
||||
end
|
||||
|
||||
-- send to main participants only messages from local occupants (skip from remote vnodes)
|
||||
if occupant and occupant_host == local_domain then
|
||||
local main_message = st.clone(stanza);
|
||||
main_message.attr.to = jid.join(jid.node(room.jid), muc_domain_prefix..'.'..main_domain);
|
||||
-- make sure we fix the from to be the real jid
|
||||
main_message.attr.from = room_jid_match_rewrite(stanza.attr.from);
|
||||
module:send(main_message);
|
||||
end
|
||||
stanza.attr.from = from; -- something prosody does internally
|
||||
|
||||
return true;
|
||||
end, 55); -- prosody check for visitor's chat is prio 50, we want to override it
|
||||
|
||||
module:hook('muc-private-message', function(event)
|
||||
-- private messaging is forbidden
|
||||
event.origin.send(st.error_reply(event.stanza, 'auth', 'forbidden',
|
||||
'Private messaging is disabled on visitor nodes'));
|
||||
return true;
|
||||
end, 10);
|
||||
|
||||
-- we calculate the stats on the configured interval (60 seconds by default)
|
||||
module:hook_global('stats-update', function ()
|
||||
local participants_count, rooms_count, visitors_count = 0, 0, 0;
|
||||
|
||||
-- iterate over all rooms
|
||||
for room in prosody.hosts[module.host].modules.muc.each_room() do
|
||||
rooms_count = rooms_count + 1;
|
||||
for _, o in room:each_occupant() do
|
||||
if not is_admin(o.bare_jid) then
|
||||
local _, host = jid.split(o.bare_jid);
|
||||
if prosody.hosts[host] then -- local hosts are visitors (including jigasi)
|
||||
visitors_count = visitors_count + 1;
|
||||
else
|
||||
participants_count = participants_count + 1;
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
measure_rooms(rooms_count);
|
||||
measure_visitors(visitors_count);
|
||||
measure_participants(participants_count);
|
||||
end);
|
||||
|
||||
-- we skip it till the main participants are added from the main prosody
|
||||
module:hook('jicofo-unlock-room', function(e)
|
||||
-- we do not block events we fired
|
||||
if e.fmuc_fired then
|
||||
return;
|
||||
end
|
||||
|
||||
return true;
|
||||
end);
|
||||
|
||||
-- handles incoming iq connect stanzas
|
||||
local function iq_from_main_handler(event)
|
||||
local origin, stanza = event.origin, event.stanza;
|
||||
|
||||
if stanza.name ~= 'iq' then
|
||||
return;
|
||||
end
|
||||
|
||||
if stanza.attr.type == 'result' and sent_iq_cache:get(stanza.attr.id) then
|
||||
sent_iq_cache:set(stanza.attr.id, nil);
|
||||
return true;
|
||||
end
|
||||
|
||||
if stanza.attr.type ~= 'set' then
|
||||
return;
|
||||
end
|
||||
|
||||
local visitors_iq = event.stanza:get_child('visitors', 'jitsi:visitors');
|
||||
if not visitors_iq then
|
||||
return;
|
||||
end
|
||||
|
||||
if stanza.attr.from ~= main_domain then
|
||||
module:log('warn', 'not from main prosody, ignore! %s', stanza);
|
||||
return true;
|
||||
end
|
||||
|
||||
local room_jid = visitors_iq.attr.room;
|
||||
local room = get_room_from_jid(room_jid_match_rewrite(room_jid));
|
||||
|
||||
if not room then
|
||||
module:log('warn', 'No room found %s', room_jid);
|
||||
return;
|
||||
end
|
||||
|
||||
local node = visitors_iq:get_child('connect');
|
||||
local fire_jicofo_unlock = true;
|
||||
local process_disconnect = false;
|
||||
|
||||
if not node then
|
||||
node = visitors_iq:get_child('update');
|
||||
fire_jicofo_unlock = false;
|
||||
end
|
||||
|
||||
if not node then
|
||||
node = visitors_iq:get_child('disconnect');
|
||||
process_disconnect = true;
|
||||
end
|
||||
|
||||
if not node then
|
||||
return;
|
||||
end
|
||||
|
||||
-- respond with successful receiving the iq
|
||||
origin.send(st.iq({
|
||||
type = 'result';
|
||||
from = stanza.attr.to;
|
||||
to = stanza.attr.from;
|
||||
id = stanza.attr.id
|
||||
}));
|
||||
|
||||
if process_disconnect then
|
||||
cancel_destroy_timer(room);
|
||||
|
||||
local main_count, visitors_count = get_occupant_counts(room);
|
||||
module:log('info', 'Will destroy:%s main_occupants:%s visitors:%s', room.jid, main_count, visitors_count);
|
||||
room:destroy(nil, 'Conference ended.');
|
||||
return true;
|
||||
end
|
||||
|
||||
-- if there is password supplied use it
|
||||
-- if this is update it will either set or remove the password
|
||||
room:set_password(node.attr.password);
|
||||
room._data.meetingId = node.attr.meetingId;
|
||||
room._data.moderator_id = node.attr.moderatorId;
|
||||
local createdTimestamp = node.attr.createdTimestamp;
|
||||
room.created_timestamp = createdTimestamp and tonumber(createdTimestamp) or nil;
|
||||
|
||||
if node.attr.lobby == 'true' then
|
||||
room._main_room_lobby_enabled = true;
|
||||
elseif node.attr.lobby == 'false' then
|
||||
room._main_room_lobby_enabled = false;
|
||||
end
|
||||
|
||||
if fire_jicofo_unlock then
|
||||
-- everything is connected allow participants to join
|
||||
module:fire_event('jicofo-unlock-room', { room = room; fmuc_fired = true; });
|
||||
end
|
||||
|
||||
return true;
|
||||
end
|
||||
module:hook('iq/host', iq_from_main_handler, 10);
|
||||
|
||||
-- Filters presences (if detected) that are with destination the main prosody
|
||||
function filter_stanza(stanza, session)
|
||||
if (stanza.name == 'presence' or stanza.name == 'message') and session.type ~= 'c2s' then
|
||||
-- we clone it so we do not affect broadcast using same stanza, sending it to clients
|
||||
local f_st = st.clone(stanza);
|
||||
f_st.skipMapping = true;
|
||||
return f_st;
|
||||
elseif stanza.name == 'presence' and session.type == 'c2s' and jid.node(stanza.attr.to) == 'focus' then
|
||||
local x = stanza:get_child('x', 'http://jabber.org/protocol/muc#user');
|
||||
if presence_check_status(x, '110') then
|
||||
return stanza; -- no filter
|
||||
end
|
||||
|
||||
-- we want to filter presences to jicofo for the main participants, skipping visitors
|
||||
-- no point of having them, but if it is the one of the first to be sent
|
||||
-- when first visitor is joining can produce the 'No hosts[from_host]' error as we
|
||||
-- rewrite the from, but we need to not do it to be able to filter it later for the s2s
|
||||
if jid.host(room_jid_match_rewrite(stanza.attr.from)) ~= local_muc_domain then
|
||||
return nil; -- returning nil filters the stanza
|
||||
end
|
||||
end
|
||||
return stanza; -- no filter
|
||||
end
|
||||
function filter_session(session)
|
||||
-- domain mapper is filtering on default priority 0, and we need it before that
|
||||
filters.add_filter(session, 'stanzas/out', filter_stanza, 2);
|
||||
end
|
||||
|
||||
filters.add_filter_hook(filter_session);
|
||||
|
||||
function route_s2s_stanza(event)
|
||||
local from_host, to_host, stanza = event.from_host, event.to_host, event.stanza;
|
||||
|
||||
if to_host ~= main_domain then
|
||||
return; -- continue with hook listeners
|
||||
end
|
||||
|
||||
if stanza.name == 'message' then
|
||||
if jid.resource(stanza.attr.to) then
|
||||
-- there is no point of delivering messages to main participants individually
|
||||
return true; -- drop it
|
||||
end
|
||||
return;
|
||||
end
|
||||
|
||||
if stanza.name == 'presence' then
|
||||
-- we want to leave only unavailable presences to go to main node
|
||||
-- all other presences from jicofo or the main participants there is no point to go to the main node
|
||||
-- they are anyway not handled
|
||||
if stanza.attr.type ~= 'unavailable' then
|
||||
return true; -- drop it
|
||||
end
|
||||
return;
|
||||
end
|
||||
end
|
||||
|
||||
-- routing to sessions in mod_s2s is -1 and -10, we want to hook before that to make sure to is correct
|
||||
-- or if we want to filter that stanza
|
||||
module:hook("route/remote", route_s2s_stanza, 10);
|
69
roles/jitsi/files/prosody/modules/mod_jibri_session.lua
Normal file
69
roles/jitsi/files/prosody/modules/mod_jibri_session.lua
Normal file
@ -0,0 +1,69 @@
|
||||
local json = require 'cjson';
|
||||
|
||||
local util = module:require 'util';
|
||||
local room_jid_match_rewrite = util.room_jid_match_rewrite;
|
||||
local get_room_from_jid = util.get_room_from_jid;
|
||||
|
||||
-- This needs to be attached to the main virtual host and the virtual host where jicofo is connected and authenticated.
|
||||
-- The first pass is the iq coming from the client where we get the creator and attach it to the app_data.
|
||||
-- The second pass is jicofo approving that and inviting jibri where we attach the session_id information to app_data
|
||||
local function attachJibriSessionId(event)
|
||||
local stanza = event.stanza;
|
||||
if stanza.name == "iq" then
|
||||
local jibri = stanza:get_child('jibri', 'http://jitsi.org/protocol/jibri');
|
||||
if jibri then
|
||||
if jibri.attr.action == 'start' then
|
||||
|
||||
local update_app_data = false;
|
||||
local app_data = jibri.attr.app_data;
|
||||
if app_data then
|
||||
app_data = json.decode(app_data);
|
||||
else
|
||||
app_data = {};
|
||||
end
|
||||
if app_data.file_recording_metadata == nil then
|
||||
app_data.file_recording_metadata = {};
|
||||
end
|
||||
|
||||
if jibri.attr.room then
|
||||
local jibri_room = jibri.attr.room;
|
||||
jibri_room = room_jid_match_rewrite(jibri_room)
|
||||
local room = get_room_from_jid(jibri_room);
|
||||
if room then
|
||||
local conference_details = {};
|
||||
conference_details["session_id"] = room._data.meetingId;
|
||||
app_data.file_recording_metadata.conference_details = conference_details;
|
||||
update_app_data = true;
|
||||
end
|
||||
else
|
||||
-- no room is because the iq received by the initiator in the room
|
||||
local session = event.origin;
|
||||
-- if a token is provided, add data to app_data
|
||||
if session ~= nil then
|
||||
local initiator = {};
|
||||
|
||||
if session.jitsi_meet_context_user ~= nil then
|
||||
initiator.id = session.jitsi_meet_context_user.id;
|
||||
end
|
||||
if session.jitsi_meet_context_group ~= nil then
|
||||
initiator.group = session.jitsi_meet_context_group;
|
||||
end
|
||||
|
||||
app_data.file_recording_metadata.initiator = initiator
|
||||
update_app_data = true;
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
if update_app_data then
|
||||
app_data = json.encode(app_data);
|
||||
jibri.attr.app_data = app_data;
|
||||
jibri:up()
|
||||
stanza:up()
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module:hook('pre-iq/full', attachJibriSessionId);
|
70
roles/jitsi/files/prosody/modules/mod_jiconop.lua
Normal file
70
roles/jitsi/files/prosody/modules/mod_jiconop.lua
Normal file
@ -0,0 +1,70 @@
|
||||
local st = require "util.stanza";
|
||||
local ext_services = module:depends("external_services");
|
||||
local get_services = ext_services.get_services;
|
||||
local services_xml = ext_services.services_xml;
|
||||
|
||||
-- Jitsi Connection Optimization
|
||||
-- gathers needed information and pushes it with a message to clients
|
||||
-- this way we skip 4 request responses during every client setup
|
||||
|
||||
local shard_name_config = module:get_option_string('shard_name');
|
||||
if shard_name_config then
|
||||
module:add_identity("server", "shard", shard_name_config);
|
||||
end
|
||||
|
||||
local region_name_config = module:get_option_string('region_name');
|
||||
if region_name_config then
|
||||
module:add_identity("server", "region", region_name_config);
|
||||
end
|
||||
|
||||
local release_number_config = module:get_option_string('release_number');
|
||||
if release_number_config then
|
||||
module:add_identity("server", "release", release_number_config);
|
||||
end
|
||||
|
||||
-- we cache the query as server identities will not change dynamically, amd use its clone every time
|
||||
local query_cache;
|
||||
|
||||
-- this is after xmpp-bind, the moment a client has resource and can be contacted
|
||||
module:hook("resource-bind", function (event)
|
||||
local session = event.session;
|
||||
|
||||
if query_cache == nil then
|
||||
-- disco info data / all identity and features
|
||||
local query = st.stanza("query", { xmlns = "http://jabber.org/protocol/disco#info" });
|
||||
local done = {};
|
||||
for _,identity in ipairs(module:get_host_items("identity")) do
|
||||
local identity_s = identity.category.."\0"..identity.type;
|
||||
if not done[identity_s] then
|
||||
query:tag("identity", identity):up();
|
||||
done[identity_s] = true;
|
||||
end
|
||||
end
|
||||
|
||||
query_cache = query;
|
||||
end
|
||||
|
||||
local query = st.clone(query_cache);
|
||||
|
||||
-- check whether room has lobby enabled and display name is required for those trying to join
|
||||
local lobby_muc_component_config = module:get_option_string('lobby_muc');
|
||||
module:context(lobby_muc_component_config):fire_event('host-disco-info-node',
|
||||
{origin = session; reply = query; node = 'lobbyrooms';});
|
||||
|
||||
-- will add a rename feature for breakout rooms.
|
||||
local breakout_rooms_muc_component_config = module:get_option_string('breakout_rooms_muc');
|
||||
if breakout_rooms_muc_component_config then
|
||||
module:context(breakout_rooms_muc_component_config):fire_event('host-disco-info-node',
|
||||
{origin = session; reply = query; node = 'breakout_rooms';});
|
||||
end
|
||||
|
||||
local stanza = st.message({
|
||||
from = module.host;
|
||||
to = session.full_jid; });
|
||||
stanza:add_child(query):up();
|
||||
|
||||
--- get turnservers and credentials
|
||||
stanza:add_child(services_xml(get_services()));
|
||||
|
||||
session.send(stanza);
|
||||
end);
|
30
roles/jitsi/files/prosody/modules/mod_jitsi_session.lua
Normal file
30
roles/jitsi/files/prosody/modules/mod_jitsi_session.lua
Normal file
@ -0,0 +1,30 @@
|
||||
-- Jitsi session information
|
||||
-- Copyright (C) 2021-present 8x8, Inc.
|
||||
module:set_global();
|
||||
|
||||
local formdecode = require "util.http".formdecode;
|
||||
|
||||
-- Extract the following parameters from the URL and set them in the session:
|
||||
-- * previd: for session resumption
|
||||
function init_session(event)
|
||||
local session, request = event.session, event.request;
|
||||
local query = request.url.query;
|
||||
|
||||
if query ~= nil then
|
||||
local params = formdecode(query);
|
||||
|
||||
-- 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
|
||||
session.previd = query and params.previd or nil;
|
||||
|
||||
-- customusername can be used with combination with "pre-jitsi-authentication" event to pre-set a known jid to a session
|
||||
session.customusername = query and params.customusername 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 "";
|
||||
end
|
||||
end
|
||||
|
||||
module:hook_global("bosh-session", init_session, 1);
|
||||
module:hook_global("websocket-session", init_session, 1);
|
32
roles/jitsi/files/prosody/modules/mod_limits_exception.lua
Normal file
32
roles/jitsi/files/prosody/modules/mod_limits_exception.lua
Normal file
@ -0,0 +1,32 @@
|
||||
-- we use async to detect Prosody 0.10 and earlier
|
||||
local have_async = pcall(require, 'util.async');
|
||||
|
||||
if not have_async then
|
||||
return;
|
||||
end
|
||||
|
||||
local unlimited_jids = module:get_option_inherited_set("unlimited_jids", {});
|
||||
|
||||
-- rises the limit of the stanza size for the unlimited jids, default is 10MB
|
||||
local unlimited_stanza_size_limit = module:get_option_number("unlimited_size", 10*1024*1024);
|
||||
|
||||
if unlimited_jids:empty() then
|
||||
return;
|
||||
end
|
||||
|
||||
module:hook("authentication-success", function (event)
|
||||
local session = event.session;
|
||||
local jid = session.username .. "@" .. session.host;
|
||||
if unlimited_jids:contains(jid) then
|
||||
if session.conn and session.conn.setlimit then
|
||||
session.conn:setlimit(0);
|
||||
elseif session.throttle then
|
||||
session.throttle = nil;
|
||||
end
|
||||
|
||||
if unlimited_stanza_size_limit and session.stream.set_stanza_size_limit then
|
||||
module:log('info', 'Setting stanza size limits for %s to %s', jid, unlimited_stanza_size_limit)
|
||||
session.stream:set_stanza_size_limit(unlimited_stanza_size_limit);
|
||||
end
|
||||
end
|
||||
end);
|
120
roles/jitsi/files/prosody/modules/mod_log_ringbuffer.lua
Normal file
120
roles/jitsi/files/prosody/modules/mod_log_ringbuffer.lua
Normal file
@ -0,0 +1,120 @@
|
||||
module:set_global();
|
||||
|
||||
local loggingmanager = require "core.loggingmanager";
|
||||
local format = require "util.format".format;
|
||||
local pposix = require "util.pposix";
|
||||
local rb = require "util.ringbuffer";
|
||||
local queue = require "util.queue";
|
||||
|
||||
local default_timestamp = "%b %d %H:%M:%S ";
|
||||
local max_chunk_size = module:get_option_number("log_ringbuffer_chunk_size", 16384);
|
||||
|
||||
local os_date = os.date;
|
||||
|
||||
local default_filename_template = "{paths.data}/ringbuffer-logs-{pid}-{count}.log";
|
||||
local render_filename = require "util.interpolation".new("%b{}", function (s) return s; end, {
|
||||
yyyymmdd = function (t)
|
||||
return os_date("%Y%m%d", t);
|
||||
end;
|
||||
hhmmss = function (t)
|
||||
return os_date("%H%M%S", t);
|
||||
end;
|
||||
});
|
||||
|
||||
local dump_count = 0;
|
||||
|
||||
local function dump_buffer(dump, filename)
|
||||
dump_count = dump_count + 1;
|
||||
local f, err = io.open(filename, "a+");
|
||||
if not f then
|
||||
module:log("error", "Unable to open output file: %s", err);
|
||||
return;
|
||||
end
|
||||
f:write(("-- Dumping log buffer at %s --\n"):format(os_date(default_timestamp)));
|
||||
dump(f);
|
||||
f:write("-- End of dump --\n\n");
|
||||
f:close();
|
||||
end
|
||||
|
||||
local function get_filename(filename_template)
|
||||
filename_template = filename_template or default_filename_template;
|
||||
return render_filename(filename_template, {
|
||||
paths = prosody.paths;
|
||||
pid = pposix.getpid();
|
||||
count = dump_count;
|
||||
time = os.time();
|
||||
});
|
||||
end
|
||||
|
||||
local function new_buffer(config)
|
||||
local write, dump;
|
||||
|
||||
if config.lines then
|
||||
local buffer = queue.new(config.lines, true);
|
||||
function write(line)
|
||||
buffer:push(line);
|
||||
end
|
||||
function dump(f)
|
||||
-- COMPAT w/0.11 - update to use :consume()
|
||||
for line in buffer.pop, buffer do
|
||||
f:write(line);
|
||||
end
|
||||
end
|
||||
else
|
||||
local buffer_size = config.size or 100*1024;
|
||||
local buffer = rb.new(buffer_size);
|
||||
function write(line)
|
||||
if not buffer:write(line) then
|
||||
if #line > buffer_size then
|
||||
buffer:discard(buffer_size);
|
||||
buffer:write(line:sub(-buffer_size));
|
||||
else
|
||||
buffer:discard(#line);
|
||||
buffer:write(line);
|
||||
end
|
||||
end
|
||||
end
|
||||
function dump(f)
|
||||
local bytes_remaining = buffer:length();
|
||||
while bytes_remaining > 0 do
|
||||
local chunk_size = math.min(bytes_remaining, max_chunk_size);
|
||||
local chunk = buffer:read(chunk_size);
|
||||
if not chunk then
|
||||
return;
|
||||
end
|
||||
f:write(chunk);
|
||||
bytes_remaining = bytes_remaining - chunk_size;
|
||||
end
|
||||
end
|
||||
end
|
||||
return write, dump;
|
||||
end
|
||||
|
||||
local function ringbuffer_log_sink_maker(sink_config)
|
||||
local write, dump = new_buffer(sink_config);
|
||||
|
||||
local timestamps = sink_config.timestamps;
|
||||
|
||||
if timestamps == true or timestamps == nil then
|
||||
timestamps = default_timestamp; -- Default format
|
||||
elseif timestamps then
|
||||
timestamps = timestamps .. " ";
|
||||
end
|
||||
|
||||
local function handler()
|
||||
dump_buffer(dump, sink_config.filename or get_filename(sink_config.filename_template));
|
||||
end
|
||||
|
||||
if sink_config.signal then
|
||||
require "util.signal".signal(sink_config.signal, handler);
|
||||
elseif sink_config.event then
|
||||
module:hook_global(sink_config.event, handler);
|
||||
end
|
||||
|
||||
return function (name, level, message, ...)
|
||||
local line = format("%s%s\t%s\t%s\n", timestamps and os_date(timestamps) or "", name, level, format(message, ...));
|
||||
write(line);
|
||||
end;
|
||||
end
|
||||
|
||||
loggingmanager.register_sink_type("ringbuffer", ringbuffer_log_sink_maker);
|
166
roles/jitsi/files/prosody/modules/mod_measure_message_count.lua
Normal file
166
roles/jitsi/files/prosody/modules/mod_measure_message_count.lua
Normal file
@ -0,0 +1,166 @@
|
||||
-- Measure the number of messages used in a meeting. Sends amplitude event.
|
||||
-- Needs to be activated under the muc component where the limit needs to be applied (main muc and breakout muc)
|
||||
-- Copyright (C) 2023-present 8x8, Inc.
|
||||
|
||||
local jid = require 'util.jid';
|
||||
local http = require 'net.http';
|
||||
local cjson_safe = require 'cjson.safe'
|
||||
|
||||
local amplitude_endpoint = module:get_option_string('amplitude_endpoint', 'https://api2.amplitude.com/2/httpapi');
|
||||
local amplitude_api_key = module:get_option_string('amplitude_api_key');
|
||||
|
||||
if not amplitude_api_key then
|
||||
module:log("warn", "No 'amplitude_api_key' option set, disabling amplitude reporting");
|
||||
return
|
||||
end
|
||||
|
||||
local muc_domain_base = module:get_option_string('muc_mapper_domain_base');
|
||||
local isBreakoutRoom = module.host == 'breakout.' .. muc_domain_base;
|
||||
|
||||
local util = module:require 'util';
|
||||
local is_healthcheck_room = util.is_healthcheck_room;
|
||||
local extract_subdomain = util.extract_subdomain;
|
||||
|
||||
module:log('info', 'Loading measure message count');
|
||||
|
||||
local shard_name = module:context(muc_domain_base):get_option_string('shard_name');
|
||||
local region_name = module:context(muc_domain_base):get_option_string('region_name');
|
||||
local release_number = module:context(muc_domain_base):get_option_string('release_number');
|
||||
local http_headers = {
|
||||
['User-Agent'] = 'Prosody ('..prosody.version..'; '..prosody.platform..')',
|
||||
['Content-Type'] = 'application/json'
|
||||
};
|
||||
|
||||
local inspect = require "inspect"
|
||||
|
||||
function table.clone(t)
|
||||
return {table.unpack(t)}
|
||||
end
|
||||
|
||||
local function event_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
|
||||
|
||||
function send_event(room)
|
||||
local user_properties = {
|
||||
shard_name = shard_name;
|
||||
region_name = region_name;
|
||||
release_number = release_number;
|
||||
};
|
||||
|
||||
local node = jid.split(room.jid);
|
||||
local subdomain, room_name = extract_subdomain(node);
|
||||
user_properties.tenant = subdomain or '/';
|
||||
user_properties.conference_name = room_name or node;
|
||||
|
||||
local event_properties = {
|
||||
messages_count = room._muc_messages_count or 0;
|
||||
polls_count = room._muc_polls_count or 0;
|
||||
tenant_mismatch = room.jitsi_meet_tenant_mismatch or false;
|
||||
};
|
||||
|
||||
if room.created_timestamp then
|
||||
event_properties.duration = (os.time() * 1000 - room.created_timestamp) / 1000;
|
||||
end
|
||||
|
||||
local event = {
|
||||
api_key = amplitude_api_key;
|
||||
events = {
|
||||
{
|
||||
user_id = room._data.meetingId;
|
||||
device_id = room._data.meetingId;
|
||||
event_type = 'conference_ended';
|
||||
event_properties = event_properties;
|
||||
user_properties = user_properties;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
local request = http.request(amplitude_endpoint, {
|
||||
headers = http_headers,
|
||||
method = "POST",
|
||||
body = cjson_safe.encode(event)
|
||||
}, event_cb);
|
||||
end
|
||||
|
||||
function on_message(event)
|
||||
local stanza = event.stanza;
|
||||
local body = stanza:get_child('body');
|
||||
|
||||
if not body then
|
||||
-- we ignore messages without body - lobby, polls ...
|
||||
return;
|
||||
end
|
||||
|
||||
local session = event.origin;
|
||||
if not session or not session.jitsi_web_query_room then
|
||||
return;
|
||||
end
|
||||
|
||||
-- get room name with tenant and find room.
|
||||
local room = get_room_by_name_and_subdomain(session.jitsi_web_query_room, session.jitsi_web_query_prefix);
|
||||
if not room then
|
||||
module:log('warn', 'No room found found for %s/%s',
|
||||
session.jitsi_web_query_prefix, session.jitsi_web_query_room);
|
||||
return;
|
||||
end
|
||||
|
||||
if not room._muc_messages_count then
|
||||
room._muc_messages_count = 0;
|
||||
end
|
||||
|
||||
room._muc_messages_count = room._muc_messages_count + 1;
|
||||
end
|
||||
|
||||
-- Conference ended, send stats
|
||||
function room_destroyed(event)
|
||||
local room, session = event.room, event.origin;
|
||||
|
||||
if is_healthcheck_room(room.jid) then
|
||||
return;
|
||||
end
|
||||
|
||||
if isBreakoutRoom then
|
||||
return;
|
||||
end
|
||||
send_event(room);
|
||||
end
|
||||
|
||||
function poll_created(event)
|
||||
local session = event.event.origin;
|
||||
|
||||
-- get room name with tenant and find room.
|
||||
local room = get_room_by_name_and_subdomain(session.jitsi_web_query_room, session.jitsi_web_query_prefix);
|
||||
if not room then
|
||||
module:log('warn', 'No room found found for %s/%s',
|
||||
session.jitsi_web_query_prefix, session.jitsi_web_query_room);
|
||||
return false;
|
||||
end
|
||||
|
||||
if not room._muc_polls_count then
|
||||
room._muc_polls_count = 0;
|
||||
end
|
||||
|
||||
room._muc_polls_count = room._muc_polls_count + 1;
|
||||
end
|
||||
|
||||
module:hook('message/full', on_message); -- private messages
|
||||
module:hook('message/bare', on_message); -- room messages
|
||||
|
||||
module:hook('muc-room-destroyed', room_destroyed, -1);
|
||||
module:hook("muc-occupant-left", function(event)
|
||||
local occupant, room = event.occupant, event.room;
|
||||
local session = event.origin;
|
||||
|
||||
if session and session.jitsi_meet_tenant_mismatch then
|
||||
room.jitsi_meet_tenant_mismatch = true;
|
||||
end
|
||||
end);
|
||||
|
||||
module:hook('poll-created', poll_created);
|
@ -0,0 +1,32 @@
|
||||
module:set_global()
|
||||
|
||||
local filters = require"util.filters";
|
||||
|
||||
local stanzas_in = module:metric(
|
||||
"counter", "received", "",
|
||||
"Stanzas received by Prosody",
|
||||
{ "session_type", "stanza_kind" }
|
||||
)
|
||||
local stanzas_out = module:metric(
|
||||
"counter", "sent", "",
|
||||
"Stanzas sent by prosody",
|
||||
{ "session_type", "stanza_kind" }
|
||||
)
|
||||
|
||||
local stanza_kinds = { message = true, presence = true, iq = true };
|
||||
|
||||
local function rate(metric_family)
|
||||
return function (stanza, session)
|
||||
if stanza.attr and not stanza.attr.xmlns and stanza_kinds[stanza.name] then
|
||||
metric_family:with_labels(session.type, stanza.name):add(1);
|
||||
end
|
||||
return stanza;
|
||||
end
|
||||
end
|
||||
|
||||
local function measure_stanza_counts(session)
|
||||
filters.add_filter(session, "stanzas/in", rate(stanzas_in));
|
||||
filters.add_filter(session, "stanzas/out", rate(stanzas_out));
|
||||
end
|
||||
|
||||
filters.add_filter_hook(measure_stanza_counts);
|
154
roles/jitsi/files/prosody/modules/mod_muc_allowners.lua
Normal file
154
roles/jitsi/files/prosody/modules/mod_muc_allowners.lua
Normal file
@ -0,0 +1,154 @@
|
||||
local filters = require 'util.filters';
|
||||
local jid = require "util.jid";
|
||||
local jid_bare = require "util.jid".bare;
|
||||
local jid_host = require "util.jid".host;
|
||||
local st = require "util.stanza";
|
||||
local um_is_admin = require "core.usermanager".is_admin;
|
||||
local util = module:require "util";
|
||||
local is_healthcheck_room = util.is_healthcheck_room;
|
||||
local is_moderated = util.is_moderated;
|
||||
local get_room_from_jid = util.get_room_from_jid;
|
||||
local presence_check_status = util.presence_check_status;
|
||||
local MUC_NS = 'http://jabber.org/protocol/muc';
|
||||
|
||||
local disable_revoke_owners;
|
||||
|
||||
local function load_config()
|
||||
disable_revoke_owners = module:get_option_boolean("allowners_disable_revoke_owners", false);
|
||||
end
|
||||
load_config();
|
||||
|
||||
local function is_admin(_jid)
|
||||
return um_is_admin(_jid, module.host);
|
||||
end
|
||||
|
||||
-- List of the bare_jids of all occupants that are currently joining (went through pre-join) and will be promoted
|
||||
-- as moderators. As pre-join (where added) and joined event (where removed) happen one after another this list should
|
||||
-- have length of 1
|
||||
local joining_moderator_participants = {};
|
||||
|
||||
module:hook("muc-occupant-pre-join", function (event)
|
||||
local room, occupant = event.room, event.occupant;
|
||||
|
||||
if is_healthcheck_room(room.jid) or is_admin(occupant.bare_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 or 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 session.jitsi_meet_domain ~= '*' and subdomain ~= session.jitsi_meet_domain then
|
||||
module:log('debug', 'skip allowners for auth user and non matching room subdomain: %s, jwt subdomain: %s',
|
||||
subdomain, session.jitsi_meet_domain);
|
||||
return;
|
||||
end
|
||||
end
|
||||
|
||||
-- mark this participant that it will be promoted and is currently joining
|
||||
joining_moderator_participants[occupant.bare_jid] = true;
|
||||
end, 2);
|
||||
|
||||
module:hook("muc-occupant-joined", function (event)
|
||||
local room, occupant = event.room, event.occupant;
|
||||
|
||||
local promote_to_moderator = joining_moderator_participants[occupant.bare_jid];
|
||||
-- clear it
|
||||
joining_moderator_participants[occupant.bare_jid] = nil;
|
||||
|
||||
if promote_to_moderator ~= nil then
|
||||
room:set_affiliation(true, occupant.bare_jid, "owner");
|
||||
end
|
||||
end, 2);
|
||||
|
||||
module:hook_global('config-reloaded', load_config);
|
||||
|
||||
-- Filters self-presences to a jid that exist in joining_participants array
|
||||
-- We want to filter those presences where we send first `participant` and just after it `moderator`
|
||||
function filter_stanza(stanza)
|
||||
-- when joining_moderator_participants is empty there is nothing to filter
|
||||
if next(joining_moderator_participants) == nil
|
||||
or not stanza.attr
|
||||
or not stanza.attr.to
|
||||
or stanza.name ~= "presence" then
|
||||
return stanza;
|
||||
end
|
||||
|
||||
-- we want to filter presences only on this host for allowners and skip anything like lobby etc.
|
||||
local host_from = jid_host(stanza.attr.from);
|
||||
if host_from ~= module.host then
|
||||
return stanza;
|
||||
end
|
||||
|
||||
local bare_to = jid_bare(stanza.attr.to);
|
||||
if stanza:get_error() and joining_moderator_participants[bare_to] then
|
||||
-- pre-join succeeded but joined did not so we need to clear cache
|
||||
joining_moderator_participants[bare_to] = nil;
|
||||
return stanza;
|
||||
end
|
||||
|
||||
local muc_x = stanza:get_child('x', MUC_NS..'#user');
|
||||
if not muc_x then
|
||||
return stanza;
|
||||
end
|
||||
|
||||
if joining_moderator_participants[bare_to] and presence_check_status(muc_x, '110') then
|
||||
-- skip the local presence for participant
|
||||
return nil;
|
||||
end
|
||||
|
||||
-- skip sending the 'participant' presences to all other people in the room
|
||||
for item in muc_x:childtags('item') do
|
||||
if joining_moderator_participants[jid_bare(item.attr.jid)] then
|
||||
return nil;
|
||||
end
|
||||
end
|
||||
|
||||
return stanza;
|
||||
end
|
||||
function filter_session(session)
|
||||
-- domain mapper is filtering on default priority 0, and we need it after that
|
||||
filters.add_filter(session, 'stanzas/out', filter_stanza, -1);
|
||||
end
|
||||
|
||||
-- enable filtering presences
|
||||
filters.add_filter_hook(filter_session);
|
||||
|
||||
-- filters any attempt to revoke owner rights on non moderated rooms
|
||||
function filter_admin_set_query(event)
|
||||
local origin, stanza = event.origin, event.stanza;
|
||||
local room_jid = jid_bare(stanza.attr.to);
|
||||
local room = get_room_from_jid(room_jid);
|
||||
|
||||
local item = stanza.tags[1].tags[1];
|
||||
local _aff = item.attr.affiliation;
|
||||
|
||||
-- if it is a moderated room we skip it
|
||||
if room and is_moderated(room.jid) then
|
||||
return nil;
|
||||
end
|
||||
|
||||
-- any revoking is disabled, everyone should be owners
|
||||
if _aff == 'none' or _aff == 'outcast' or _aff == 'member' then
|
||||
origin.send(st.error_reply(stanza, "auth", "forbidden"));
|
||||
return true;
|
||||
end
|
||||
end
|
||||
|
||||
if not disable_revoke_owners then
|
||||
-- default prosody priority for handling these is -2
|
||||
module:hook("iq-set/bare/http://jabber.org/protocol/muc#admin:query", filter_admin_set_query, 5);
|
||||
module:hook("iq-set/host/http://jabber.org/protocol/muc#admin:query", filter_admin_set_query, 5);
|
||||
end
|
88
roles/jitsi/files/prosody/modules/mod_muc_auth_ban.lua
Normal file
88
roles/jitsi/files/prosody/modules/mod_muc_auth_ban.lua
Normal file
@ -0,0 +1,88 @@
|
||||
-- Can be used to ban users based on external http service
|
||||
-- Copyright (C) 2023-present 8x8, Inc.
|
||||
|
||||
local ACCESS_MANAGER_URL = module:get_option_string("muc_prosody_jitsi_access_manager_url");
|
||||
if not ACCESS_MANAGER_URL then
|
||||
module:log("warn", "No 'muc_prosody_jitsi_access_manager_url' option set, disabling module");
|
||||
return
|
||||
end
|
||||
|
||||
local json = require "cjson.safe";
|
||||
local http = require "net.http";
|
||||
local inspect = require 'inspect';
|
||||
|
||||
local ban_check_count = module:measure("muc_auth_ban_check", "rate")
|
||||
local ban_check_users_banned_count = module:measure("muc_auth_ban_users_banned", "rate")
|
||||
|
||||
-- we will cache banned tokens to avoid extra requests
|
||||
-- on destroying session, websocket retries 2 more times before giving up
|
||||
local cache = require "util.cache".new(100);
|
||||
|
||||
local CACHE_DURATION = 5*60; -- 5 mins
|
||||
|
||||
local cache_timer = module:add_timer(CACHE_DURATION, function()
|
||||
for k, v in cache:items() do
|
||||
if socket.gettime() > v + CACHE_DURATION then
|
||||
cache:set(k, nil);
|
||||
end
|
||||
end
|
||||
|
||||
if cache:count() > 0 then
|
||||
-- rescheduling the timer
|
||||
return CACHE_DURATION;
|
||||
end
|
||||
|
||||
-- skipping return value stops the timer
|
||||
end);
|
||||
|
||||
local function shouldAllow(session)
|
||||
local token = session.auth_token;
|
||||
|
||||
if token ~= nil then
|
||||
-- module:log("debug", "Checking whether user should be banned ")
|
||||
|
||||
-- cached tokens are banned
|
||||
if cache:get(token) then
|
||||
return false;
|
||||
end
|
||||
|
||||
-- TODO: do this only for enabled customers
|
||||
ban_check_count();
|
||||
local function cb(content, code, response, request)
|
||||
if code == 200 then
|
||||
|
||||
local r = json.decode(content)
|
||||
if r['access'] ~= nil and r['access'] == false then
|
||||
module:log("info", "User is banned room:%s tenant:%s user_id:%s group:%s",
|
||||
session.jitsi_meet_room, session.jitsi_web_query_prefix,
|
||||
inspect(session.jitsi_meet_context_user), session.jitsi_meet_context_group);
|
||||
|
||||
ban_check_users_banned_count();
|
||||
|
||||
session:close();
|
||||
|
||||
-- if the cache is empty and the timer is not running reschedule it
|
||||
if cache:count() == 0 then
|
||||
cache_timer:reschedule(CACHE_DURATION);
|
||||
end
|
||||
|
||||
cache:set(token, socket.gettime());
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local request_headers = {}
|
||||
request_headers['Authorization'] = 'Bearer ' .. token;
|
||||
|
||||
http.request(ACCESS_MANAGER_URL, {
|
||||
headers = request_headers,
|
||||
method = "GET",
|
||||
}, cb);
|
||||
|
||||
return true;
|
||||
end
|
||||
end
|
||||
|
||||
prosody.events.add_handler("jitsi-access-ban-check", function(session)
|
||||
return shouldAllow(session)
|
||||
end)
|
635
roles/jitsi/files/prosody/modules/mod_muc_breakout_rooms.lua
Normal file
635
roles/jitsi/files/prosody/modules/mod_muc_breakout_rooms.lua
Normal file
@ -0,0 +1,635 @@
|
||||
-- This module is added under the main virtual host domain
|
||||
-- It needs a breakout rooms muc component
|
||||
--
|
||||
-- VirtualHost "jitmeet.example.com"
|
||||
-- modules_enabled = {
|
||||
-- "muc_breakout_rooms"
|
||||
-- }
|
||||
-- breakout_rooms_muc = "breakout.jitmeet.example.com"
|
||||
-- main_muc = "muc.jitmeet.example.com"
|
||||
--
|
||||
-- Component "breakout.jitmeet.example.com" "muc"
|
||||
-- restrict_room_creation = true
|
||||
-- storage = "memory"
|
||||
-- admins = { "focusUser@auth.jitmeet.example.com" }
|
||||
-- muc_room_locking = false
|
||||
-- muc_room_default_public_jids = true
|
||||
--
|
||||
|
||||
module:depends('room_destroy');
|
||||
|
||||
-- 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', 'Breakout rooms will not work with Prosody version 0.10 or less.');
|
||||
return;
|
||||
end
|
||||
|
||||
local jid_node = require 'util.jid'.node;
|
||||
local jid_host = require 'util.jid'.host;
|
||||
local jid_split = require 'util.jid'.split;
|
||||
local json = require 'cjson.safe';
|
||||
local st = require 'util.stanza';
|
||||
local uuid_gen = require 'util.uuid'.generate;
|
||||
|
||||
local util = module:require 'util';
|
||||
local internal_room_jid_match_rewrite = util.internal_room_jid_match_rewrite;
|
||||
local is_healthcheck_room = util.is_healthcheck_room;
|
||||
local process_host_module = util.process_host_module;
|
||||
|
||||
local BREAKOUT_ROOMS_IDENTITY_TYPE = 'breakout_rooms';
|
||||
-- Available breakout room functionality
|
||||
local RENAME_FEATURE = 'http://jitsi.org/protocol/breakout_rooms#rename';
|
||||
-- only send at most this often updates on breakout rooms to avoid flooding.
|
||||
local BROADCAST_ROOMS_INTERVAL = .3;
|
||||
-- close conference after this amount of seconds if all leave.
|
||||
local ROOMS_TTL_IF_ALL_LEFT = 5;
|
||||
local JSON_TYPE_ADD_BREAKOUT_ROOM = 'features/breakout-rooms/add';
|
||||
local JSON_TYPE_MOVE_TO_ROOM_REQUEST = 'features/breakout-rooms/move-to-room';
|
||||
local JSON_TYPE_REMOVE_BREAKOUT_ROOM = 'features/breakout-rooms/remove';
|
||||
local JSON_TYPE_RENAME_BREAKOUT_ROOM = 'features/breakout-rooms/rename';
|
||||
local JSON_TYPE_UPDATE_BREAKOUT_ROOMS = 'features/breakout-rooms/update';
|
||||
|
||||
local main_muc_component_config = module:get_option_string('main_muc');
|
||||
if main_muc_component_config == nil then
|
||||
module:log('error', 'breakout rooms not enabled missing main_muc config');
|
||||
return ;
|
||||
end
|
||||
local breakout_rooms_muc_component_config = module:get_option_string('breakout_rooms_muc', 'breakout.'..module.host);
|
||||
|
||||
module:depends('jitsi_session');
|
||||
|
||||
local breakout_rooms_muc_service;
|
||||
local main_muc_service;
|
||||
|
||||
-- Maps a breakout room jid to the main room jid
|
||||
local main_rooms_map = {};
|
||||
|
||||
-- Utility functions
|
||||
|
||||
function get_main_room_jid(room_jid)
|
||||
local _, host = jid_split(room_jid);
|
||||
|
||||
return
|
||||
host == main_muc_component_config
|
||||
and room_jid
|
||||
or main_rooms_map[room_jid];
|
||||
end
|
||||
|
||||
function get_main_room(room_jid)
|
||||
local main_room_jid = get_main_room_jid(room_jid);
|
||||
|
||||
return main_muc_service.get_room_from_jid(main_room_jid), main_room_jid;
|
||||
end
|
||||
|
||||
function get_room_from_jid(room_jid)
|
||||
local host = jid_host(room_jid);
|
||||
|
||||
return
|
||||
host == main_muc_component_config
|
||||
and main_muc_service.get_room_from_jid(room_jid)
|
||||
or breakout_rooms_muc_service.get_room_from_jid(room_jid);
|
||||
end
|
||||
|
||||
function send_json_msg(to_jid, json_msg)
|
||||
local stanza = st.message({ from = breakout_rooms_muc_component_config; to = to_jid; })
|
||||
:tag('json-message', { xmlns = 'http://jitsi.org/jitmeet' }):text(json_msg):up();
|
||||
module:send(stanza);
|
||||
end
|
||||
|
||||
function get_participants(room)
|
||||
local participants = {};
|
||||
|
||||
if room then
|
||||
for room_nick, occupant in room:each_occupant() do
|
||||
-- Filter focus as we keep it as a hidden participant
|
||||
if jid_node(occupant.jid) ~= 'focus' then
|
||||
local display_name = occupant:get_presence():get_child_text(
|
||||
'nick', 'http://jabber.org/protocol/nick');
|
||||
local real_nick = internal_room_jid_match_rewrite(room_nick);
|
||||
participants[real_nick] = {
|
||||
jid = occupant.jid,
|
||||
role = occupant.role,
|
||||
displayName = display_name
|
||||
};
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return participants;
|
||||
end
|
||||
|
||||
function broadcast_breakout_rooms(room_jid)
|
||||
local main_room = get_main_room(room_jid);
|
||||
|
||||
if not main_room or main_room.broadcast_timer then
|
||||
return;
|
||||
end
|
||||
|
||||
-- Only send each BROADCAST_ROOMS_INTERVAL seconds to prevent flooding of messages.
|
||||
main_room.broadcast_timer = module:add_timer(BROADCAST_ROOMS_INTERVAL, function()
|
||||
local main_room, main_room_jid = get_main_room(room_jid);
|
||||
|
||||
if not main_room then
|
||||
return;
|
||||
end
|
||||
|
||||
main_room.broadcast_timer = nil;
|
||||
|
||||
local real_jid = internal_room_jid_match_rewrite(main_room_jid);
|
||||
local real_node = jid_node(real_jid);
|
||||
local rooms = {
|
||||
[real_node] = {
|
||||
isMainRoom = true,
|
||||
id = real_node,
|
||||
jid = real_jid,
|
||||
name = main_room._data.subject,
|
||||
participants = get_participants(main_room)
|
||||
};
|
||||
}
|
||||
|
||||
for breakout_room_jid, subject in pairs(main_room._data.breakout_rooms or {}) do
|
||||
local breakout_room = breakout_rooms_muc_service.get_room_from_jid(breakout_room_jid);
|
||||
local breakout_room_node = jid_node(breakout_room_jid)
|
||||
|
||||
rooms[breakout_room_node] = {
|
||||
id = breakout_room_node,
|
||||
jid = breakout_room_jid,
|
||||
name = subject,
|
||||
participants = {}
|
||||
}
|
||||
|
||||
-- The room may not physically exist yet.
|
||||
if breakout_room then
|
||||
rooms[breakout_room_node].participants = get_participants(breakout_room);
|
||||
end
|
||||
end
|
||||
|
||||
local json_msg, error = json.encode({
|
||||
type = BREAKOUT_ROOMS_IDENTITY_TYPE,
|
||||
event = JSON_TYPE_UPDATE_BREAKOUT_ROOMS,
|
||||
roomCounter = main_room._data.breakout_rooms_counter,
|
||||
rooms = rooms
|
||||
});
|
||||
|
||||
if not json_msg then
|
||||
module:log('error', 'not broadcasting breakout room information room:%s error:%s', main_room_jid, error);
|
||||
return;
|
||||
end
|
||||
|
||||
for _, occupant in main_room:each_occupant() do
|
||||
if jid_node(occupant.jid) ~= 'focus' then
|
||||
send_json_msg(occupant.jid, json_msg)
|
||||
end
|
||||
end
|
||||
|
||||
for breakout_room_jid in pairs(main_room._data.breakout_rooms or {}) do
|
||||
local room = breakout_rooms_muc_service.get_room_from_jid(breakout_room_jid);
|
||||
if room then
|
||||
for _, occupant in room:each_occupant() do
|
||||
if jid_node(occupant.jid) ~= 'focus' then
|
||||
send_json_msg(occupant.jid, json_msg)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end);
|
||||
end
|
||||
|
||||
|
||||
-- Managing breakout rooms
|
||||
|
||||
function create_breakout_room(room_jid, subject)
|
||||
local main_room, main_room_jid = get_main_room(room_jid);
|
||||
local breakout_room_jid = uuid_gen() .. '@' .. breakout_rooms_muc_component_config;
|
||||
|
||||
if not main_room._data.breakout_rooms then
|
||||
main_room._data.breakout_rooms = {};
|
||||
main_room._data.breakout_rooms_counter = 0;
|
||||
end
|
||||
main_room._data.breakout_rooms_counter = main_room._data.breakout_rooms_counter + 1;
|
||||
main_room._data.breakout_rooms[breakout_room_jid] = subject;
|
||||
main_room._data.breakout_rooms_active = true;
|
||||
-- Make room persistent - not to be destroyed - if all participants join breakout rooms.
|
||||
main_room:set_persistent(true);
|
||||
main_room:save(true);
|
||||
|
||||
main_rooms_map[breakout_room_jid] = main_room_jid;
|
||||
broadcast_breakout_rooms(main_room_jid);
|
||||
end
|
||||
|
||||
function destroy_breakout_room(room_jid, message)
|
||||
local main_room, main_room_jid = get_main_room(room_jid);
|
||||
|
||||
if room_jid == main_room_jid then
|
||||
return;
|
||||
end
|
||||
|
||||
local breakout_room = breakout_rooms_muc_service.get_room_from_jid(room_jid);
|
||||
|
||||
if breakout_room then
|
||||
message = message or 'Breakout room removed.';
|
||||
breakout_room:destroy(main_room and main_room_jid or nil, message);
|
||||
end
|
||||
if main_room then
|
||||
if main_room._data.breakout_rooms then
|
||||
main_room._data.breakout_rooms[room_jid] = nil;
|
||||
end
|
||||
main_room:save(true);
|
||||
|
||||
main_rooms_map[room_jid] = nil;
|
||||
broadcast_breakout_rooms(main_room_jid);
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function rename_breakout_room(room_jid, name)
|
||||
local main_room, main_room_jid = get_main_room(room_jid);
|
||||
|
||||
if room_jid == main_room_jid then
|
||||
return;
|
||||
end
|
||||
|
||||
if main_room then
|
||||
if main_room._data.breakout_rooms then
|
||||
main_room._data.breakout_rooms[room_jid] = name;
|
||||
local breakout_room = breakout_rooms_muc_service.get_room_from_jid(room_jid);
|
||||
|
||||
if breakout_room then
|
||||
breakout_room:set_subject(breakout_room.jid, name);
|
||||
end
|
||||
|
||||
end
|
||||
main_room:save(true);
|
||||
broadcast_breakout_rooms(main_room_jid);
|
||||
end
|
||||
end
|
||||
|
||||
-- Handling events
|
||||
|
||||
function on_message(event)
|
||||
local session = event.origin;
|
||||
|
||||
-- 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 not session or not session.jitsi_web_query_room then
|
||||
return false;
|
||||
end
|
||||
|
||||
local message = event.stanza:get_child(BREAKOUT_ROOMS_IDENTITY_TYPE);
|
||||
|
||||
if not message then
|
||||
return false;
|
||||
end
|
||||
|
||||
-- get room name with tenant and find room
|
||||
local room = get_room_by_name_and_subdomain(session.jitsi_web_query_room, session.jitsi_web_query_prefix);
|
||||
|
||||
if not room then
|
||||
module:log('warn', 'No room found for %s/%s',
|
||||
session.jitsi_web_query_prefix, session.jitsi_web_query_room);
|
||||
return false;
|
||||
end
|
||||
|
||||
-- check that the participant requesting is a moderator and is an occupant in the room
|
||||
local from = event.stanza.attr.from;
|
||||
local occupant = room:get_occupant_by_real_jid(from);
|
||||
|
||||
if not occupant then
|
||||
-- Check if the participant is in any breakout room.
|
||||
for breakout_room_jid in pairs(room._data.breakout_rooms or {}) do
|
||||
local breakout_room = breakout_rooms_muc_service.get_room_from_jid(breakout_room_jid);
|
||||
if breakout_room then
|
||||
occupant = breakout_room:get_occupant_by_real_jid(from);
|
||||
if occupant then
|
||||
break;
|
||||
end
|
||||
end
|
||||
end
|
||||
if not occupant then
|
||||
module:log('warn', 'No occupant %s found for %s', from, room.jid);
|
||||
return false;
|
||||
end
|
||||
end
|
||||
|
||||
if occupant.role ~= 'moderator' then
|
||||
module:log('warn', 'Occupant %s is not moderator and not allowed this operation for %s', from, room.jid);
|
||||
return false;
|
||||
end
|
||||
|
||||
if message.attr.type == JSON_TYPE_ADD_BREAKOUT_ROOM then
|
||||
create_breakout_room(room.jid, message.attr.subject);
|
||||
return true;
|
||||
elseif message.attr.type == JSON_TYPE_REMOVE_BREAKOUT_ROOM then
|
||||
destroy_breakout_room(message.attr.breakoutRoomJid);
|
||||
return true;
|
||||
elseif message.attr.type == JSON_TYPE_RENAME_BREAKOUT_ROOM then
|
||||
rename_breakout_room(message.attr.breakoutRoomJid, message.attr.subject);
|
||||
return true;
|
||||
elseif message.attr.type == JSON_TYPE_MOVE_TO_ROOM_REQUEST then
|
||||
local participant_jid = message.attr.participantJid;
|
||||
local target_room_jid = message.attr.roomJid;
|
||||
|
||||
local json_msg, error = json.encode({
|
||||
type = BREAKOUT_ROOMS_IDENTITY_TYPE,
|
||||
event = JSON_TYPE_MOVE_TO_ROOM_REQUEST,
|
||||
roomJid = target_room_jid
|
||||
});
|
||||
|
||||
if not json_msg then
|
||||
module:log('error', 'skip sending request room:%s error:%s', room.jid, error);
|
||||
end
|
||||
|
||||
send_json_msg(participant_jid, json_msg)
|
||||
return true;
|
||||
end
|
||||
|
||||
-- return error.
|
||||
return false;
|
||||
end
|
||||
|
||||
function on_breakout_room_pre_create(event)
|
||||
local breakout_room = event.room;
|
||||
local main_room, main_room_jid = get_main_room(breakout_room.jid);
|
||||
|
||||
-- Only allow existent breakout rooms to be started.
|
||||
-- Authorisation of breakout rooms is done by their random uuid name
|
||||
if main_room and main_room._data.breakout_rooms and main_room._data.breakout_rooms[breakout_room.jid] then
|
||||
breakout_room:set_subject(breakout_room.jid, main_room._data.breakout_rooms[breakout_room.jid]);
|
||||
else
|
||||
module:log('debug', 'Invalid breakout room %s will not be created.', breakout_room.jid);
|
||||
breakout_room:destroy(main_room_jid, 'Breakout room is invalid.');
|
||||
return true;
|
||||
end
|
||||
end
|
||||
|
||||
function on_occupant_joined(event)
|
||||
local room = event.room;
|
||||
|
||||
if is_healthcheck_room(room.jid) then
|
||||
return;
|
||||
end
|
||||
|
||||
local main_room, main_room_jid = get_main_room(room.jid);
|
||||
|
||||
if main_room and main_room._data.breakout_rooms_active then
|
||||
if jid_node(event.occupant.jid) ~= 'focus' then
|
||||
broadcast_breakout_rooms(main_room_jid);
|
||||
end
|
||||
|
||||
-- Prevent closing all rooms if a participant has joined (see on_occupant_left).
|
||||
if main_room.close_timer then
|
||||
main_room.close_timer:stop();
|
||||
main_room.close_timer = nil;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function exist_occupants_in_room(room)
|
||||
if not room then
|
||||
return false;
|
||||
end
|
||||
for _, occupant in room:each_occupant() do
|
||||
if jid_node(occupant.jid) ~= 'focus' then
|
||||
return true;
|
||||
end
|
||||
end
|
||||
|
||||
return false;
|
||||
end
|
||||
|
||||
function exist_occupants_in_rooms(main_room)
|
||||
if exist_occupants_in_room(main_room) then
|
||||
return true;
|
||||
end
|
||||
for breakout_room_jid in pairs(main_room._data.breakout_rooms or {}) do
|
||||
local room = breakout_rooms_muc_service.get_room_from_jid(breakout_room_jid);
|
||||
if exist_occupants_in_room(room) then
|
||||
return true;
|
||||
end
|
||||
end
|
||||
|
||||
return false;
|
||||
end
|
||||
|
||||
function on_occupant_left(event)
|
||||
local room_jid = event.room.jid;
|
||||
|
||||
if is_healthcheck_room(room_jid) then
|
||||
return;
|
||||
end
|
||||
|
||||
local main_room, main_room_jid = get_main_room(room_jid);
|
||||
|
||||
if not main_room then
|
||||
return;
|
||||
end
|
||||
|
||||
if main_room._data.breakout_rooms_active and jid_node(event.occupant.jid) ~= 'focus' then
|
||||
broadcast_breakout_rooms(main_room_jid);
|
||||
end
|
||||
|
||||
-- Close the conference if all left for good.
|
||||
if main_room._data.breakout_rooms_active and not main_room.close_timer and not exist_occupants_in_rooms(main_room) then
|
||||
main_room.close_timer = module:add_timer(ROOMS_TTL_IF_ALL_LEFT, function()
|
||||
-- we need to look up again the room as till the timer is fired, the room maybe already destroyed/recreated
|
||||
-- and we will have the old instance
|
||||
local main_room, main_room_jid = get_main_room(room_jid);
|
||||
if main_room and main_room.close_timer then
|
||||
prosody.events.fire_event("maybe-destroy-room", {
|
||||
room = main_room;
|
||||
reason = 'All occupants left.';
|
||||
caller = module:get_name();
|
||||
});
|
||||
end
|
||||
end);
|
||||
end
|
||||
end
|
||||
|
||||
-- Stop other modules from destroying room if breakout rooms not empty
|
||||
function handle_maybe_destroy_main_room(event)
|
||||
local main_room = event.room;
|
||||
local caller = event.caller;
|
||||
|
||||
if caller == module:get_name() then
|
||||
-- we were the one that requested the deletion. Do not override.
|
||||
return nil; -- stop room destruction
|
||||
end
|
||||
|
||||
-- deletion was requested by another module. Check for break room occupants.
|
||||
for breakout_room_jid, _ in pairs(main_room._data.breakout_rooms or {}) do
|
||||
local breakout_room = breakout_rooms_muc_service.get_room_from_jid(breakout_room_jid);
|
||||
if breakout_room and breakout_room:has_occupant() then
|
||||
module:log('info', 'Suppressing room destroy. Breakout room still occupied %s', breakout_room_jid);
|
||||
return true; -- stop room destruction
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module:hook_global("maybe-destroy-room", handle_maybe_destroy_main_room)
|
||||
|
||||
|
||||
function on_main_room_destroyed(event)
|
||||
local main_room = event.room;
|
||||
|
||||
if is_healthcheck_room(main_room.jid) then
|
||||
return;
|
||||
end
|
||||
|
||||
for breakout_room_jid in pairs(main_room._data.breakout_rooms or {}) do
|
||||
destroy_breakout_room(breakout_room_jid, event.reason)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- Module operations
|
||||
|
||||
-- operates on already loaded breakout rooms muc module
|
||||
function process_breakout_rooms_muc_loaded(breakout_rooms_muc, host_module)
|
||||
module:log('debug', 'Breakout rooms muc loaded');
|
||||
|
||||
-- Advertise the breakout rooms component so clients can pick up the address and use it
|
||||
module:add_identity('component', BREAKOUT_ROOMS_IDENTITY_TYPE, breakout_rooms_muc_component_config);
|
||||
|
||||
-- Tag the disco#info response with available features of breakout rooms.
|
||||
host_module:hook('host-disco-info-node', function (event)
|
||||
local session, reply, node = event.origin, event.reply, event.node;
|
||||
if node == BREAKOUT_ROOMS_IDENTITY_TYPE and session.jitsi_web_query_room then
|
||||
reply:tag('feature', { var = RENAME_FEATURE }):up();
|
||||
end
|
||||
event.exists = true;
|
||||
end);
|
||||
|
||||
breakout_rooms_muc_service = breakout_rooms_muc;
|
||||
module:log("info", "Hook to muc events on %s", breakout_rooms_muc_component_config);
|
||||
host_module:hook('message/host', on_message);
|
||||
host_module:hook('muc-occupant-joined', on_occupant_joined);
|
||||
host_module:hook('muc-occupant-left', on_occupant_left);
|
||||
host_module:hook('muc-room-pre-create', on_breakout_room_pre_create);
|
||||
|
||||
host_module:hook('muc-disco#info', function (event)
|
||||
local room = event.room;
|
||||
local main_room, main_room_jid = get_main_room(room.jid);
|
||||
|
||||
-- Breakout room metadata.
|
||||
table.insert(event.form, {
|
||||
name = 'muc#roominfo_isbreakout';
|
||||
label = 'Is this a breakout room?';
|
||||
type = "boolean";
|
||||
});
|
||||
event.formdata['muc#roominfo_isbreakout'] = true;
|
||||
table.insert(event.form, {
|
||||
name = 'muc#roominfo_breakout_main_room';
|
||||
label = 'The main room associated with this breakout room';
|
||||
});
|
||||
event.formdata['muc#roominfo_breakout_main_room'] = main_room_jid;
|
||||
|
||||
-- If the main room has a lobby, make it so this breakout room also uses it.
|
||||
if (main_room and main_room._data.lobbyroom and main_room:get_members_only()) then
|
||||
table.insert(event.form, {
|
||||
name = 'muc#roominfo_lobbyroom';
|
||||
label = 'Lobby room jid';
|
||||
});
|
||||
event.formdata['muc#roominfo_lobbyroom'] = main_room._data.lobbyroom;
|
||||
end
|
||||
end);
|
||||
|
||||
host_module:hook("muc-config-form", function(event)
|
||||
local room = event.room;
|
||||
local _, main_room_jid = get_main_room(room.jid);
|
||||
|
||||
-- Breakout room metadata.
|
||||
table.insert(event.form, {
|
||||
name = 'muc#roominfo_isbreakout';
|
||||
label = 'Is this a breakout room?';
|
||||
type = "boolean";
|
||||
value = true;
|
||||
});
|
||||
|
||||
table.insert(event.form, {
|
||||
name = 'muc#roominfo_breakout_main_room';
|
||||
label = 'The main room associated with this breakout room';
|
||||
value = main_room_jid;
|
||||
});
|
||||
end);
|
||||
|
||||
local room_mt = breakout_rooms_muc_service.room_mt;
|
||||
|
||||
room_mt.get_members_only = function(room)
|
||||
local main_room = get_main_room(room.jid);
|
||||
|
||||
if not main_room then
|
||||
module:log('error', 'No main room (%s)!', room.jid);
|
||||
return false;
|
||||
end
|
||||
|
||||
return main_room.get_members_only(main_room)
|
||||
end
|
||||
|
||||
-- we base affiliations (roles) in breakout rooms muc component to be based on the roles in the main muc
|
||||
room_mt.get_affiliation = function(room, jid)
|
||||
local main_room, _ = get_main_room(room.jid);
|
||||
|
||||
if not 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 = main_room.get_affiliation(main_room, jid);
|
||||
if role then
|
||||
return role;
|
||||
end
|
||||
|
||||
return 'none';
|
||||
end
|
||||
end
|
||||
|
||||
-- process or waits to process the breakout rooms muc component
|
||||
process_host_module(breakout_rooms_muc_component_config, function(host_module, host)
|
||||
module:log('info', 'Breakout rooms component created %s', host);
|
||||
|
||||
local muc_module = prosody.hosts[host].modules.muc;
|
||||
|
||||
if muc_module then
|
||||
process_breakout_rooms_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_breakout_rooms_muc_loaded(prosody.hosts[host].modules.muc, host_module);
|
||||
end
|
||||
end);
|
||||
end
|
||||
end);
|
||||
|
||||
-- operates on already loaded main muc module
|
||||
function process_main_muc_loaded(main_muc, host_module)
|
||||
module:log('debug', 'Main muc loaded');
|
||||
|
||||
main_muc_service = main_muc;
|
||||
module:log("info", "Hook to muc events on %s", main_muc_component_config);
|
||||
host_module:hook('muc-occupant-joined', on_occupant_joined);
|
||||
host_module:hook('muc-occupant-left', on_occupant_left);
|
||||
host_module:hook('muc-room-destroyed', on_main_room_destroyed);
|
||||
end
|
||||
|
||||
-- process or waits to process the main muc component
|
||||
process_host_module(main_muc_component_config, function(host_module, host)
|
||||
local muc_module = prosody.hosts[host].modules.muc;
|
||||
|
||||
if muc_module then
|
||||
process_main_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_main_muc_loaded(prosody.hosts[host].modules.muc, host_module);
|
||||
end
|
||||
end);
|
||||
end
|
||||
end);
|
125
roles/jitsi/files/prosody/modules/mod_muc_call.lua
Normal file
125
roles/jitsi/files/prosody/modules/mod_muc_call.lua
Normal file
@ -0,0 +1,125 @@
|
||||
local jid = require "util.jid"
|
||||
local extract_subdomain = module:require "util".extract_subdomain;
|
||||
|
||||
-- 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 = extract_subdomain(node);
|
||||
|
||||
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.
|
||||
-- FIX ME: luacheck warning 581
|
||||
-- not (x == y)' can be replaced by 'x ~= y' (if neither side is a table or NaN)
|
||||
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))
|
||||
module:fire_event('jitsi-call-invite', { stanza = event.stanza; url = url; call_id = 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")
|
||||
module:fire_event('jitsi-call-cancel', {
|
||||
stanza = event.stanza;
|
||||
url = url;
|
||||
reason = string.lower(status);
|
||||
call_id = 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()
|
||||
module:fire_event('jitsi-call-missed', { stanza = event.stanza; call_id = 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
|
||||
);
|
106
roles/jitsi/files/prosody/modules/mod_muc_census.lua
Normal file
106
roles/jitsi/files/prosody/modules/mod_muc_census.lua
Normal file
@ -0,0 +1,106 @@
|
||||
-- provides an http endpoint at /room-census that reports list of rooms with the
|
||||
-- number of members and created date in this JSON format:
|
||||
--
|
||||
-- {
|
||||
-- "room_census": [
|
||||
-- {
|
||||
-- "room_name": "<muc name>",
|
||||
-- "participants": <# participants>,
|
||||
-- "created_time": <unix timestamp>,
|
||||
-- },
|
||||
-- ...
|
||||
-- ]
|
||||
-- }
|
||||
--
|
||||
-- to activate, add "muc_census" to the modules_enabled table in prosody.cfg.lua
|
||||
--
|
||||
-- warning: this module is unprotected and intended for server admin use only.
|
||||
-- when enabled, make sure to secure the endpoint at the web server or via
|
||||
-- network filters
|
||||
|
||||
local jid = require "util.jid";
|
||||
local json = require 'cjson.safe';
|
||||
local iterators = require "util.iterators";
|
||||
local util = module:require "util";
|
||||
local is_healthcheck_room = util.is_healthcheck_room;
|
||||
|
||||
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;
|
||||
|
||||
-- required parameter for custom muc component prefix, defaults to "conference"
|
||||
local muc_domain_prefix = module:get_option_string("muc_mapper_domain_prefix", "conference");
|
||||
|
||||
local leaked_rooms = 0;
|
||||
|
||||
--- handles request to get number of participants in all rooms
|
||||
-- @return GET response
|
||||
function handle_get_room_census(event)
|
||||
local host_session = prosody.hosts[muc_domain_prefix .. "." .. tostring(module.host)]
|
||||
if not host_session or not host_session.modules.muc then
|
||||
return { status_code = 400; }
|
||||
end
|
||||
|
||||
room_data = {}
|
||||
leaked_rooms = 0;
|
||||
for room in host_session.modules.muc.each_room() do
|
||||
if not is_healthcheck_room(room.jid) then
|
||||
local occupants = room._occupants;
|
||||
local participant_count = 0;
|
||||
local missing_connections_count = 0;
|
||||
|
||||
if occupants then
|
||||
for _, o in room:each_occupant() do
|
||||
participant_count = participant_count + 1;
|
||||
|
||||
-- let's check whether that occupant has connection in the full_sessions of prosody
|
||||
-- attempt to detect leaked occupants/rooms.
|
||||
if prosody.full_sessions[o.jid] == nil then
|
||||
missing_connections_count = missing_connections_count + 1;
|
||||
end
|
||||
end
|
||||
participant_count = participant_count - 1; -- subtract focus
|
||||
end
|
||||
|
||||
local leaked = false;
|
||||
if participant_count > 0 and missing_connections_count == participant_count then
|
||||
leaked = true;
|
||||
leaked_rooms = leaked_rooms + 1;
|
||||
end
|
||||
|
||||
table.insert(room_data, {
|
||||
room_name = room.jid;
|
||||
participants = participant_count;
|
||||
created_time = room.created_timestamp;
|
||||
leaked = leaked;
|
||||
});
|
||||
end
|
||||
end
|
||||
|
||||
census_resp = json.encode({
|
||||
room_census = room_data;
|
||||
});
|
||||
return { status_code = 200; body = census_resp }
|
||||
end
|
||||
|
||||
function module.load()
|
||||
module:depends("http");
|
||||
module:provides("http", {
|
||||
default_path = "/";
|
||||
route = {
|
||||
["GET room-census"] = function (event) return async_handler_wrapper(event,handle_get_room_census) end;
|
||||
};
|
||||
});
|
||||
end
|
||||
|
||||
-- we calculate the stats on the configured interval (60 seconds by default)
|
||||
local measure_leaked_rooms = module:measure('leaked_rooms', 'amount');
|
||||
module:hook_global('stats-update', function ()
|
||||
measure_leaked_rooms(leaked_rooms);
|
||||
end);
|
107
roles/jitsi/files/prosody/modules/mod_muc_domain_mapper.lua
Normal file
107
roles/jitsi/files/prosody/modules/mod_muc_domain_mapper.lua
Normal file
@ -0,0 +1,107 @@
|
||||
-- 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 filters = require "util.filters";
|
||||
|
||||
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
|
||||
|
||||
local log_not_allowed_errors = module:get_option_boolean('muc_mapper_log_not_allowed_errors', false);
|
||||
|
||||
local util = module:require "util";
|
||||
local room_jid_match_rewrite = util.room_jid_match_rewrite;
|
||||
local internal_room_jid_match_rewrite = util.internal_room_jid_match_rewrite;
|
||||
|
||||
-- We must filter stanzas in order to hook in to all incoming and outgoing messaging which skips the stanza routers
|
||||
function filter_stanza(stanza, session)
|
||||
if stanza.skipMapping then
|
||||
return stanza;
|
||||
end
|
||||
|
||||
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 = room_jid_match_rewrite(conf.attr.room, stanza)
|
||||
end
|
||||
end
|
||||
if stanza.attr.to then
|
||||
stanza.attr.to = room_jid_match_rewrite(stanza.attr.to, stanza)
|
||||
end
|
||||
if stanza.attr.from then
|
||||
stanza.attr.from = internal_room_jid_match_rewrite(stanza.attr.from, stanza)
|
||||
end
|
||||
|
||||
if log_not_allowed_errors and stanza.name == 'presence' and stanza.attr.type == 'error' then
|
||||
local error = stanza:get_child('error');
|
||||
if error and error.attr.type == 'cancel'
|
||||
and error:get_child('not-allowed', 'urn:ietf:params:xml:ns:xmpp-stanzas')
|
||||
and not session.jitsi_not_allowed_logged then
|
||||
session.jitsi_not_allowed_logged = true;
|
||||
session.log('error', 'Not allowed presence %s', stanza);
|
||||
end
|
||||
end
|
||||
end
|
||||
return stanza;
|
||||
end
|
||||
|
||||
function filter_session(session)
|
||||
-- module:log("warn", "Session filters applied");
|
||||
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 = room_jid_match_rewrite(stanza.attr.to, stanza)
|
||||
end
|
||||
end
|
||||
|
||||
local function incoming_stanza_rewriter(event)
|
||||
local stanza = event.stanza;
|
||||
if stanza.attr.from then
|
||||
stanza.attr.from = internal_room_jid_match_rewrite(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
|
113
roles/jitsi/files/prosody/modules/mod_muc_end_meeting.lua
Normal file
113
roles/jitsi/files/prosody/modules/mod_muc_end_meeting.lua
Normal file
@ -0,0 +1,113 @@
|
||||
-- A global module which can be used as http endpoint to end meetings. The provided token
|
||||
--- in the request is verified whether it has the right to do so.
|
||||
-- Copyright (C) 2023-present 8x8, Inc.
|
||||
|
||||
module:set_global();
|
||||
|
||||
local util = module:require "util";
|
||||
local async_handler_wrapper = util.async_handler_wrapper;
|
||||
local room_jid_match_rewrite = util.room_jid_match_rewrite;
|
||||
local get_room_from_jid = util.get_room_from_jid;
|
||||
local starts_with = util.starts_with;
|
||||
|
||||
local neturl = require "net.url";
|
||||
local parse = neturl.parseQuery;
|
||||
|
||||
-- will be initialized once the main virtual host module is initialized
|
||||
local token_util;
|
||||
|
||||
local muc_domain_base = module:get_option_string("muc_mapper_domain_base");
|
||||
|
||||
local asapKeyServer = module:get_option_string("prosody_password_public_key_repo_url", "");
|
||||
|
||||
local event_count = module:measure("muc_end_meeting_rate", "rate")
|
||||
local event_count_success = module:measure("muc_end_meeting_success", "rate")
|
||||
|
||||
function verify_token(token)
|
||||
if token == nil then
|
||||
module:log("warn", "no token provided");
|
||||
return false;
|
||||
end
|
||||
|
||||
local session = {};
|
||||
session.auth_token = token;
|
||||
local verified, reason, msg = token_util:process_and_verify_token(session);
|
||||
if not verified then
|
||||
module:log("warn", "not a valid token %s %s", tostring(reason), tostring(msg));
|
||||
return false;
|
||||
end
|
||||
return true;
|
||||
end
|
||||
|
||||
function handle_terminate_meeting (event)
|
||||
module:log("info", "Request for terminate meeting received: reqid %s", event.request.headers["request_id"])
|
||||
event_count()
|
||||
if not event.request.url.query then
|
||||
return { status_code = 400 };
|
||||
end
|
||||
local params = parse(event.request.url.query);
|
||||
local conference = params["conference"];
|
||||
local room_jid;
|
||||
|
||||
if conference then
|
||||
room_jid = room_jid_match_rewrite(conference)
|
||||
else
|
||||
module:log('warn', "conference param was not provided")
|
||||
return { status_code = 400 };
|
||||
end
|
||||
|
||||
-- verify access
|
||||
local token = event.request.headers["authorization"]
|
||||
if not token then
|
||||
module:log("error", "Authorization header was not provided for conference %s", conference)
|
||||
return { status_code = 401 };
|
||||
end
|
||||
if starts_with(token, 'Bearer ') then
|
||||
token = token:sub(8, #token)
|
||||
else
|
||||
module:log("error", "Authorization header is invalid")
|
||||
return { status_code = 401 };
|
||||
end
|
||||
|
||||
if not verify_token(token, room_jid) then
|
||||
return { status_code = 401 };
|
||||
end
|
||||
|
||||
local room = get_room_from_jid(room_jid);
|
||||
if not room then
|
||||
module:log("warn", "Room not found")
|
||||
return { status_code = 404 };
|
||||
else
|
||||
module:log("info", "Destroy room jid %s", room.jid)
|
||||
room:destroy(nil, "The meeting has been terminated")
|
||||
end
|
||||
event_count_success()
|
||||
return { status_code = 200 };
|
||||
end
|
||||
|
||||
|
||||
-- module API called on virtual host added, passing the host module
|
||||
function module.add_host(host_module)
|
||||
if host_module.host == muc_domain_base then
|
||||
-- the main virtual host
|
||||
module:log("info", "Initialize token_util using %s", host_module.host)
|
||||
|
||||
token_util = module:require "token/util".new(host_module);
|
||||
|
||||
if asapKeyServer then
|
||||
-- init token util with our asap keyserver
|
||||
token_util:set_asap_key_server(asapKeyServer)
|
||||
end
|
||||
|
||||
module:log("info", "Adding http handler for /end-meeting on %s", host_module.host);
|
||||
host_module:depends("http");
|
||||
host_module:provides("http", {
|
||||
default_path = "/";
|
||||
route = {
|
||||
["POST end-meeting"] = function(event)
|
||||
return async_handler_wrapper(event, handle_terminate_meeting)
|
||||
end;
|
||||
};
|
||||
});
|
||||
end
|
||||
end
|
27
roles/jitsi/files/prosody/modules/mod_muc_filter_access.lua
Normal file
27
roles/jitsi/files/prosody/modules/mod_muc_filter_access.lua
Normal file
@ -0,0 +1,27 @@
|
||||
-- Restricts access to a muc component to certain domains
|
||||
-- Copyright (C) 2023-present 8x8, Inc.
|
||||
|
||||
-- a list of (authenticated)domains that can access rooms(send presence)
|
||||
local whitelist = module:get_option_set("muc_filter_whitelist");
|
||||
|
||||
if not whitelist then
|
||||
module:log("warn", "No 'muc_filter_whitelist' option set, disabling muc_filter_access, plugin inactive");
|
||||
return
|
||||
end
|
||||
|
||||
local jid_split = require "util.jid".split;
|
||||
|
||||
local function incoming_presence_filter(event)
|
||||
local stanza = event.stanza;
|
||||
local _, domain, _ = jid_split(stanza.attr.from);
|
||||
|
||||
if not stanza.attr.from or not whitelist:contains(domain) then
|
||||
-- Filter presence
|
||||
module:log("error", "Filtering unauthorized presence: %s", stanza:top_tag());
|
||||
return true;
|
||||
end
|
||||
end
|
||||
|
||||
for _, jid_type in ipairs({ "host", "bare", "full" }) do
|
||||
module:hook("presence/"..jid_type, incoming_presence_filter, 2000);
|
||||
end
|
211
roles/jitsi/files/prosody/modules/mod_muc_flip.lua
Normal file
211
roles/jitsi/files/prosody/modules/mod_muc_flip.lua
Normal file
@ -0,0 +1,211 @@
|
||||
-- Allows flipping device. When a presence contains flip_device tag
|
||||
-- and the used jwt matches the id(session.jitsi_meet_context_user.id) of another user this is indication that the user
|
||||
-- is moving from one device to another. The flip feature should be present and enabled in the token features.
|
||||
-- Copyright (C) 2023-present 8x8, Inc.
|
||||
|
||||
local oss_util = module:require "util";
|
||||
local is_healthcheck_room = oss_util.is_healthcheck_room;
|
||||
local process_host_module = oss_util.process_host_module;
|
||||
local um_is_admin = require "core.usermanager".is_admin;
|
||||
local inspect = require('inspect');
|
||||
local jid_bare = require "util.jid".bare;
|
||||
local jid = require "util.jid";
|
||||
local MUC_NS = "http://jabber.org/protocol/muc";
|
||||
|
||||
local lobby_host;
|
||||
local lobby_muc_service;
|
||||
|
||||
local lobby_muc_component_config = 'lobby.' .. module:get_option_string("muc_mapper_domain_base");
|
||||
if lobby_muc_component_config == nil then
|
||||
module:log('error', 'lobby not enabled missing lobby_muc config');
|
||||
return ;
|
||||
end
|
||||
|
||||
local function is_admin(occupant_jid)
|
||||
return um_is_admin(occupant_jid, module.host);
|
||||
end
|
||||
|
||||
local function remove_flip_tag(stanza)
|
||||
stanza:maptags(function(tag)
|
||||
if tag and tag.name == "flip_device" then
|
||||
-- module:log("debug", "Removing %s tag from presence stanza!", tag.name);
|
||||
return nil;
|
||||
else
|
||||
return tag;
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
-- Make user that switch devices bypass lobby or password.
|
||||
-- A user is considered to join from another device if the
|
||||
-- id from jwt is the same as another occupant and the presence
|
||||
-- stanza has flip_device tag
|
||||
module:hook("muc-occupant-pre-join", function(event)
|
||||
local room, occupant = event.room, event.occupant;
|
||||
local session = event.origin;
|
||||
local stanza = event.stanza;
|
||||
if is_healthcheck_room(room.jid) or is_admin(occupant.bare_jid) then
|
||||
return ;
|
||||
end
|
||||
local flip_device_tag = stanza:get_child("flip_device");
|
||||
if session.jitsi_meet_context_user and session.jitsi_meet_context_user.id then
|
||||
local participants = room._data.participants_details or {};
|
||||
local id = session.jitsi_meet_context_user.id;
|
||||
local first_device_occ_nick = participants[id];
|
||||
if flip_device_tag then
|
||||
if first_device_occ_nick and session.jitsi_meet_context_features.flip and (session.jitsi_meet_context_features.flip == true or session.jitsi_meet_context_features.flip == "true") then
|
||||
room._data.kicked_participant_nick = first_device_occ_nick;
|
||||
room._data.flip_participant_nick = occupant.nick;
|
||||
-- allow participant from flip device to bypass Lobby
|
||||
local occupant_jid = stanza.attr.from;
|
||||
local affiliation = room:get_affiliation(occupant_jid);
|
||||
if not affiliation or affiliation == 'none' or affiliation == 'member' then
|
||||
-- module:log("debug", "Bypass lobby invitee %s", occupant_jid)
|
||||
occupant.role = "participant";
|
||||
room:set_affiliation(true, jid_bare(occupant_jid), "member")
|
||||
room:save_occupant(occupant);
|
||||
end
|
||||
|
||||
if room:get_password() then
|
||||
-- bypass password on the flip device
|
||||
local join = stanza:get_child("x", MUC_NS);
|
||||
if not join then
|
||||
join = stanza:tag("x", { xmlns = MUC_NS });
|
||||
end
|
||||
local password = join:get_child("password", MUC_NS);
|
||||
if password then
|
||||
join:maptags(
|
||||
function(tag)
|
||||
for k, v in pairs(tag) do
|
||||
if k == "name" and v == "password" then
|
||||
return nil
|
||||
end
|
||||
end
|
||||
return tag
|
||||
end);
|
||||
end
|
||||
join:tag("password", { xmlns = MUC_NS }):text(room:get_password());
|
||||
end
|
||||
elseif not session.jitsi_meet_context_features.flip or session.jitsi_meet_context_features.flip == false or session.jitsi_meet_context_features.flip == "false" then
|
||||
module:log("warn", "Flip device tag present without jwt permission")
|
||||
--remove flip_device tag if somebody wants to abuse this feature
|
||||
remove_flip_tag(stanza)
|
||||
else
|
||||
module:log("warn", "Flip device tag present without user from different device")
|
||||
--remove flip_device tag if somebody wants to abuse this feature
|
||||
remove_flip_tag(stanza)
|
||||
end
|
||||
end
|
||||
-- update authenticated participant list
|
||||
participants[id] = occupant.nick;
|
||||
room._data.participants_details = participants
|
||||
-- module:log("debug", "current details list %s", inspect(participants))
|
||||
else
|
||||
if flip_device_tag then
|
||||
module:log("warn", "Flip device tag present for a guest user")
|
||||
-- remove flip_device tag because a guest want to do a sneaky join
|
||||
remove_flip_tag(stanza)
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
-- Kick participant from the the first device from the main room and lobby if applies
|
||||
-- and transfer role from the previous participant, this will take care of the grant
|
||||
-- moderation case
|
||||
module:hook("muc-occupant-joined", function(event)
|
||||
local room, occupant = event.room, event.occupant;
|
||||
if is_healthcheck_room(room.jid) or is_admin(occupant.bare_jid) then
|
||||
return;
|
||||
end
|
||||
|
||||
if room._data.flip_participant_nick and occupant.nick == room._data.flip_participant_nick then
|
||||
-- make joining participant from flip device have the same role and affiliation as for the previous device
|
||||
local kicked_occupant = room:get_occupant_by_nick(room._data.kicked_participant_nick);
|
||||
|
||||
if not kicked_occupant then
|
||||
module:log("info", "Kick participant not found, nick %s from main room jid %s",
|
||||
room._data.kicked_participant_nick, room.jid)
|
||||
return;
|
||||
end
|
||||
|
||||
local initial_affiliation = room:get_affiliation(kicked_occupant.jid) or "member";
|
||||
-- module:log("debug", "Transfer affiliation %s to occupant jid %s", initial_affiliation, occupant.jid)
|
||||
room:set_affiliation(true, occupant.bare_jid, initial_affiliation)
|
||||
if initial_affiliation == "owner" then
|
||||
event.occupant.role = "moderator";
|
||||
elseif initial_affiliation == "member" then
|
||||
event.occupant.role = "participant";
|
||||
end
|
||||
-- Kick participant from the first device from the main room
|
||||
local kicked_participant_node_jid = jid.split(kicked_occupant.jid);
|
||||
module:log("info", "Kick participant jid %s nick %s from main room jid %s", kicked_occupant.jid, room._data.kicked_participant_nick, room.jid)
|
||||
room:set_role(true, room._data.kicked_participant_nick, 'none')
|
||||
room:save_occupant(occupant);
|
||||
-- Kick participant from the first device from the lobby room
|
||||
if room._data.lobbyroom then
|
||||
local lobby_room_jid = room._data.lobbyroom;
|
||||
local lobby_room = lobby_muc_service.get_room_from_jid(lobby_room_jid)
|
||||
for _, occupant in lobby_room:each_occupant() do
|
||||
local node = jid.split(occupant.jid);
|
||||
if kicked_participant_node_jid == node then
|
||||
module:log("info", "Kick participant from lobby %s", occupant.jid)
|
||||
lobby_room:set_role(true, occupant.nick, 'none')
|
||||
end
|
||||
end
|
||||
end
|
||||
event.room._data.flip_participant_nick = nil
|
||||
event.room._data.kicked_participant_nick = nil;
|
||||
end
|
||||
end,-2)
|
||||
|
||||
-- Update the local table after a participant leaves
|
||||
module:hook("muc-occupant-left", function(event)
|
||||
local occupant = event.occupant;
|
||||
local session = event.origin;
|
||||
if is_healthcheck_room(event.room.jid) or is_admin(occupant.bare_jid) then
|
||||
return ;
|
||||
end
|
||||
if session and session.jitsi_meet_context_user and session.jitsi_meet_context_user.id then
|
||||
local id = session.jitsi_meet_context_user.id
|
||||
local participants = event.room._data.participants_details or {};
|
||||
local occupant_left_nick = participants[id]
|
||||
if occupant_left_nick == occupant.nick then
|
||||
participants[id] = nil
|
||||
event.room._data.participants_details = participants
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
-- Add a flip_device tag on the unavailable presence from the kicked participant in order to silent the notifications
|
||||
module:hook('muc-broadcast-presence', function(event)
|
||||
local kicked_participant_nick = event.room._data.kicked_participant_nick
|
||||
local stanza = event.stanza;
|
||||
if kicked_participant_nick and stanza.attr.from == kicked_participant_nick and stanza.attr.type == 'unavailable' then
|
||||
-- module:log("debug", "Add flip_device tag for presence unavailable from occupant nick %s", kicked_participant_nick)
|
||||
stanza:tag("flip_device"):up();
|
||||
end
|
||||
end)
|
||||
|
||||
function process_lobby_muc_loaded(lobby_muc, host_module)
|
||||
module:log('info', 'Lobby muc loaded');
|
||||
lobby_muc_service = lobby_muc;
|
||||
lobby_host = module:context(host_module);
|
||||
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);
|
6
roles/jitsi/files/prosody/modules/mod_muc_hide_all.lua
Normal file
6
roles/jitsi/files/prosody/modules/mod_muc_hide_all.lua
Normal file
@ -0,0 +1,6 @@
|
||||
-- This module makes all MUCs in Prosody unavailable on disco#items query
|
||||
-- Copyright (C) 2023-present 8x8, Inc.
|
||||
|
||||
module:hook("muc-room-pre-create", function(event)
|
||||
event.room:set_hidden(true);
|
||||
end, -1);
|
191
roles/jitsi/files/prosody/modules/mod_muc_jigasi_invite.lua
Normal file
191
roles/jitsi/files/prosody/modules/mod_muc_jigasi_invite.lua
Normal file
@ -0,0 +1,191 @@
|
||||
-- A http endpoint to invite jigasi to a meeting via http endpoint
|
||||
-- jwt is used to validate access
|
||||
-- Copyright (C) 2023-present 8x8, Inc.
|
||||
|
||||
local jid_split = require "util.jid".split;
|
||||
local hashes = require "util.hashes";
|
||||
local random = require "util.random";
|
||||
local st = require("util.stanza");
|
||||
local json = require 'cjson.safe';
|
||||
local util = module:require "util";
|
||||
local async_handler_wrapper = util.async_handler_wrapper;
|
||||
local process_host_module = util.process_host_module;
|
||||
|
||||
local muc_domain_base = module:get_option_string("muc_mapper_domain_base");
|
||||
|
||||
-- This module chooses jigasi from the brewery room, so it needs information for the configured brewery
|
||||
local muc_domain = module:get_option_string("muc_internal_domain_base", 'internal.auth.' .. muc_domain_base);
|
||||
|
||||
local jigasi_brewery_room_jid = module:get_option_string("muc_jigasi_brewery_jid", 'jigasibrewery@' .. muc_domain);
|
||||
|
||||
local jigasi_bare_jid = module:get_option_string("muc_jigasi_jid", "jigasi@auth." .. muc_domain_base);
|
||||
local focus_jid = module:get_option_string("muc_jicofo_brewery_jid", jigasi_brewery_room_jid .. "/focus");
|
||||
|
||||
local main_muc_service;
|
||||
local JSON_CONTENT_TYPE = "application/json";
|
||||
|
||||
local event_count = module:measure("muc_invite_jigasi_rate", "rate")
|
||||
local event_count_success = module:measure("muc_invite_jigasi_success", "rate")
|
||||
local ASAP_KEY_SERVER = module:get_option_string("prosody_password_public_key_repo_url", "");
|
||||
local token_util = module:require "token/util".new(module);
|
||||
if ASAP_KEY_SERVER then
|
||||
-- init token util with our asap keyserver
|
||||
token_util:set_asap_key_server(ASAP_KEY_SERVER)
|
||||
end
|
||||
|
||||
local function invite_jigasi(conference, phone_no)
|
||||
local jigasi_brewery_room = main_muc_service.get_room_from_jid(jigasi_brewery_room_jid);
|
||||
if not jigasi_brewery_room then
|
||||
module:log("error", "Jigasi brewery room not found")
|
||||
return 404, 'Brewery room was not found'
|
||||
end
|
||||
module:log("info", "Invite jigasi from %s to join conference %s and outbound phone_no %s", jigasi_brewery_room.jid, conference, phone_no)
|
||||
|
||||
--select least stressed Jigasi
|
||||
local least_stressed_value = math.huge;
|
||||
local least_stressed_jigasi_jid;
|
||||
for occupant_jid, occupant in jigasi_brewery_room:each_occupant() do
|
||||
local _, _, resource = jid_split(occupant_jid);
|
||||
if resource ~= 'focus' then
|
||||
local occ = occupant:get_presence();
|
||||
local stats_child = occ:get_child("stats", "http://jitsi.org/protocol/colibri")
|
||||
|
||||
local is_sip_jigasi = true;
|
||||
for stats_tag in stats_child:children() do
|
||||
if stats_tag.attr.name == 'supports_sip' and stats_tag.attr.value == 'false' then
|
||||
is_sip_jigasi = false;
|
||||
end
|
||||
end
|
||||
|
||||
if is_sip_jigasi then
|
||||
for stats_tag in stats_child:children() do
|
||||
if stats_tag.attr.name == 'stress_level' then
|
||||
local stress_level = tonumber(stats_tag.attr.value);
|
||||
module:log("debug", "Stressed level %s %s ", stress_level, occupant_jid)
|
||||
if stress_level < least_stressed_value then
|
||||
least_stressed_jigasi_jid = occupant_jid
|
||||
least_stressed_value = stress_level
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
module:log("debug", "Least stressed jigasi selected jid %s value %s", least_stressed_jigasi_jid, least_stressed_value)
|
||||
if not least_stressed_jigasi_jid then
|
||||
module:log("error", "Cannot invite jigasi from room %s", jigasi_brewery_room.jid)
|
||||
return 404, 'Jigasi not found'
|
||||
end
|
||||
|
||||
-- invite Jigasi to join the conference
|
||||
local _, _, jigasi_res = jid_split(least_stressed_jigasi_jid)
|
||||
local jigasi_full_jid = jigasi_bare_jid .. "/" .. jigasi_res;
|
||||
local stanza_id = hashes.sha256(random.bytes(8), true);
|
||||
|
||||
local invite_jigasi_stanza = st.iq({ xmlns = "jabber:client", type = "set", to = jigasi_full_jid, from = focus_jid, id = stanza_id })
|
||||
:tag("dial", { xmlns = "urn:xmpp:rayo:1", from = "fromnumber", to = phone_no })
|
||||
:tag("header", { xmlns = "urn:xmpp:rayo:1", name = "JvbRoomName", value = conference })
|
||||
|
||||
module:log("debug", "Invite jigasi stanza %s", invite_jigasi_stanza)
|
||||
jigasi_brewery_room:route_stanza(invite_jigasi_stanza);
|
||||
return 200
|
||||
end
|
||||
|
||||
local function is_token_valid(token)
|
||||
if token == nil then
|
||||
module:log("warn", "no token provided");
|
||||
return false;
|
||||
end
|
||||
|
||||
local session = {};
|
||||
session.auth_token = token;
|
||||
local verified, reason, msg = token_util:process_and_verify_token(session);
|
||||
if not verified then
|
||||
module:log("warn", "not a valid token %s %s", tostring(reason), tostring(msg));
|
||||
return false;
|
||||
end
|
||||
return true;
|
||||
end
|
||||
|
||||
local function handle_jigasi_invite(event)
|
||||
module:log("debug", "Request for invite jigasi received: reqId %s", event.request.headers["request_id"])
|
||||
event_count()
|
||||
local request = event.request;
|
||||
-- verify access
|
||||
local token = event.request.headers["authorization"]
|
||||
if not token then
|
||||
module:log("error", "Authorization header was not provided for conference %s", conference)
|
||||
return { status_code = 401 };
|
||||
end
|
||||
if util.starts_with(token, 'Bearer ') then
|
||||
token = token:sub(8, #token)
|
||||
else
|
||||
module:log("error", "Authorization header is invalid")
|
||||
return { status_code = 401 };
|
||||
end
|
||||
if not is_token_valid(token) then
|
||||
return { status_code = 401 };
|
||||
end
|
||||
|
||||
-- verify payload
|
||||
if request.headers.content_type ~= JSON_CONTENT_TYPE
|
||||
or (not request.body or #request.body == 0) then
|
||||
module:log("warn", "Wrong content type: %s or missing payload", request.headers.content_type);
|
||||
return { status_code = 400; }
|
||||
end
|
||||
local payload, error = json.decode(request.body);
|
||||
|
||||
if not payload then
|
||||
module:log('error', 'Cannot decode json error:%s', error);
|
||||
return { status_code = 400; }
|
||||
end
|
||||
|
||||
local conference = payload["conference"];
|
||||
local phone_no = payload["phoneNo"];
|
||||
if not conference then
|
||||
module:log("warn", "Missing conference param")
|
||||
return { status_code = 400; }
|
||||
end
|
||||
if not phone_no then
|
||||
module:log("warn", "Missing phone no param")
|
||||
return { status_code = 400; }
|
||||
end
|
||||
|
||||
--invite jigasi
|
||||
local status_code, error_msg = invite_jigasi(conference, phone_no)
|
||||
|
||||
if not error_msg then
|
||||
event_count_success()
|
||||
return { status_code = 200 }
|
||||
else
|
||||
return { status_code = status_code, body = json.encode({ error = error_msg }) }
|
||||
end
|
||||
end
|
||||
|
||||
module:log("info", "Adding http handler for /invite-jigasi on %s", module.host);
|
||||
module:depends("http");
|
||||
module:provides("http", {
|
||||
default_path = "/";
|
||||
route = {
|
||||
["POST invite-jigasi"] = function(event)
|
||||
return async_handler_wrapper(event, handle_jigasi_invite)
|
||||
end;
|
||||
};
|
||||
});
|
||||
|
||||
process_host_module(muc_domain, function(_, host)
|
||||
local muc_module = prosody.hosts[host].modules.muc;
|
||||
if muc_module then
|
||||
main_muc_service = muc_module;
|
||||
module:log('info', 'Found main_muc_service: %s', main_muc_service);
|
||||
else
|
||||
module:log('info', 'Will wait for muc to be available');
|
||||
prosody.hosts[host].events.add_handler('module-loaded', function(event)
|
||||
if (event.module == 'muc') then
|
||||
main_muc_service = prosody.hosts[host].modules.muc;
|
||||
module:log('info', 'Found(on loaded) main_muc_service: %s', main_muc_service);
|
||||
end
|
||||
end);
|
||||
end
|
||||
end);
|
||||
|
158
roles/jitsi/files/prosody/modules/mod_muc_kick_jigasi.lua
Normal file
158
roles/jitsi/files/prosody/modules/mod_muc_kick_jigasi.lua
Normal file
@ -0,0 +1,158 @@
|
||||
-- http endpoint to kick participants, access is based on provided jwt token
|
||||
-- the correct jigasi we fined based on the display name and the number provided
|
||||
-- Copyright (C) 2023-present 8x8, Inc.
|
||||
|
||||
local util = module:require "util";
|
||||
local async_handler_wrapper = util.async_handler_wrapper;
|
||||
local is_sip_jigasi = util.is_sip_jigasi;
|
||||
local starts_with = util.starts_with;
|
||||
local formdecode = require "util.http".formdecode;
|
||||
local urlencode = require "util.http".urlencode;
|
||||
local jid = require "util.jid";
|
||||
local json = require 'cjson.safe';
|
||||
|
||||
local muc_domain_prefix = module:get_option_string("muc_mapper_domain_prefix", "conference");
|
||||
|
||||
local muc_domain_base = module:get_option_string("muc_mapper_domain_base");
|
||||
if not muc_domain_base then
|
||||
module:log("warn", "No 'muc_domain_base' option set, disabling kick check endpoint.");
|
||||
return ;
|
||||
end
|
||||
|
||||
local json_content_type = "application/json";
|
||||
|
||||
local token_util = module:require "token/util".new(module);
|
||||
|
||||
local asapKeyServer = module:get_option_string('prosody_password_public_key_repo_url', '');
|
||||
if asapKeyServer == '' then
|
||||
module:log('warn', 'No "prosody_password_public_key_repo_url" option set, disabling kick endpoint.');
|
||||
return ;
|
||||
end
|
||||
|
||||
token_util:set_asap_key_server(asapKeyServer);
|
||||
|
||||
--- Verifies the token
|
||||
-- @param token the token we received
|
||||
-- @param room_address the full room address jid
|
||||
-- @return true if values are ok or false otherwise
|
||||
function verify_token(token, room_address)
|
||||
|
||||
if token == nil then
|
||||
module:log("warn", "no token provided for %s", room_address);
|
||||
return false;
|
||||
end
|
||||
|
||||
local session = {};
|
||||
session.auth_token = token;
|
||||
local verified, reason, msg = token_util:process_and_verify_token(session);
|
||||
if not verified then
|
||||
module:log("warn", "not a valid token %s %s for %s", tostring(reason), tostring(msg), room_address);
|
||||
return false;
|
||||
end
|
||||
|
||||
return true;
|
||||
end
|
||||
|
||||
|
||||
-- Validates the request by checking for required url param room and
|
||||
-- validates the token provided with the request
|
||||
-- @param request - The request to validate.
|
||||
-- @return [error_code, room]
|
||||
local function validate_and_get_room(request)
|
||||
if not request.url.query then
|
||||
module:log("warn", "No query");
|
||||
return 400, nil;
|
||||
end
|
||||
|
||||
local params = formdecode(request.url.query);
|
||||
local room_name = urlencode(params.room) or "";
|
||||
local subdomain = urlencode(params.prefix) or "";
|
||||
|
||||
if not room_name then
|
||||
module:log("warn", "Missing room param for %s", room_name);
|
||||
return 400, nil;
|
||||
end
|
||||
|
||||
local room_address = jid.join(room_name, muc_domain_prefix.."."..muc_domain_base);
|
||||
|
||||
if subdomain and subdomain ~= "" then
|
||||
room_address = "["..subdomain.."]"..room_address;
|
||||
end
|
||||
|
||||
-- verify access
|
||||
local token = request.headers["authorization"]
|
||||
|
||||
if token and starts_with(token,'Bearer ') then
|
||||
token = token:sub(8,#token)
|
||||
end
|
||||
|
||||
if not verify_token(token, room_address) then
|
||||
return 403, nil;
|
||||
end
|
||||
|
||||
local room = get_room_from_jid(room_address);
|
||||
|
||||
if not room then
|
||||
module:log("warn", "No room found for %s", room_address);
|
||||
return 404, nil;
|
||||
else
|
||||
return 200, room;
|
||||
end
|
||||
end
|
||||
|
||||
function handle_kick_participant (event)
|
||||
local request = event.request;
|
||||
if request.headers.content_type ~= json_content_type
|
||||
or (not request.body or #request.body == 0) then
|
||||
module:log("warn", "Wrong content type: %s", request.headers.content_type);
|
||||
return { status_code = 400; }
|
||||
end
|
||||
|
||||
local params, error = json.decode(request.body);
|
||||
if not params then
|
||||
module:log("warn", "Missing params error:%s", error);
|
||||
return { status_code = 400; }
|
||||
end
|
||||
|
||||
local number = params["number"];
|
||||
|
||||
if not number then
|
||||
module:log("warn", "Missing number param");
|
||||
return { status_code = 400; };
|
||||
end
|
||||
|
||||
local error_code, room = validate_and_get_room(request);
|
||||
|
||||
if error_code and error_code ~= 200 then
|
||||
module:log("error", "Error validating %s", error_code);
|
||||
return { error_code = 400; }
|
||||
end
|
||||
|
||||
if not room then
|
||||
return { status_code = 404; }
|
||||
end
|
||||
|
||||
for _, occupant in room:each_occupant() do
|
||||
local pr = occupant:get_presence();
|
||||
local displayName = pr:get_child_text(
|
||||
'nick', 'http://jabber.org/protocol/nick');
|
||||
|
||||
if is_sip_jigasi(pr) and displayName and starts_with(displayName, number) then
|
||||
room:set_role(true, occupant.nick, nil);
|
||||
module:log('info', 'Occupant kicked %s from %s', occupant.nick, room.jid);
|
||||
return { status_code = 200; }
|
||||
end
|
||||
end
|
||||
|
||||
-- not found participant to kick
|
||||
return { status_code = 404; };
|
||||
end
|
||||
|
||||
module:log("info","Adding http handler for /kick-participant on %s", module.host);
|
||||
module:depends("http");
|
||||
module:provides("http", {
|
||||
default_path = "/";
|
||||
route = {
|
||||
["PUT kick-participant"] = function (event) return async_handler_wrapper(event, handle_kick_participant) end;
|
||||
};
|
||||
});
|
103
roles/jitsi/files/prosody/modules/mod_muc_limit_messages.lua
Normal file
103
roles/jitsi/files/prosody/modules/mod_muc_limit_messages.lua
Normal file
@ -0,0 +1,103 @@
|
||||
-- A module to limit the number of messages in a meeting
|
||||
-- Needs to be activated under the muc component where the limit needs to be applied
|
||||
-- Copyright (C) 2023-present 8x8, Inc.
|
||||
|
||||
local id = require 'util.id';
|
||||
local st = require 'util.stanza';
|
||||
|
||||
local get_room_by_name_and_subdomain = module:require 'util'.get_room_by_name_and_subdomain;
|
||||
|
||||
local count;
|
||||
local check_token;
|
||||
|
||||
local function load_config()
|
||||
count = module:get_option_number('muc_limit_messages_count');
|
||||
check_token = module:get_option_boolean('muc_limit_messages_check_token', false);
|
||||
end
|
||||
load_config();
|
||||
|
||||
if not count then
|
||||
module:log('warn', "No 'muc_limit_messages_count' option set, disabling module");
|
||||
return
|
||||
end
|
||||
|
||||
module:log('info', 'Loaded muc limits for %s, limit:%s, will check for authenticated users:%s',
|
||||
module.host, count, check_token);
|
||||
|
||||
local error_text = 'The message limit for the room has been reached. Messaging is now disabled.';
|
||||
|
||||
function on_message(event)
|
||||
local stanza = event.stanza;
|
||||
local body = stanza:get_child('body');
|
||||
-- we ignore any non groupchat message without a body
|
||||
if not body then
|
||||
if stanza.attr.type ~= 'groupchat' then -- lobby messages
|
||||
return;
|
||||
else
|
||||
-- we want to pass through only polls answers
|
||||
local json_data = stanza:get_child_text('json-message', 'http://jitsi.org/jitmeet');
|
||||
if json_data and string.find(json_data, 'answer-poll', 1, true) then
|
||||
return;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local session = event.origin;
|
||||
if not session or not session.jitsi_web_query_room then
|
||||
-- if this is a message from visitor, pass it through. Limits are applied in the visitor node.
|
||||
if event.origin.type == 's2sin' then
|
||||
return;
|
||||
end
|
||||
|
||||
return false;
|
||||
end
|
||||
|
||||
-- get room name with tenant and find room
|
||||
local room = get_room_by_name_and_subdomain(session.jitsi_web_query_room, session.jitsi_web_query_prefix);
|
||||
if not room then
|
||||
module:log('warn', 'No room found found for %s/%s',
|
||||
session.jitsi_web_query_prefix, session.jitsi_web_query_room);
|
||||
return false;
|
||||
end
|
||||
|
||||
if check_token and session.auth_token then
|
||||
-- there is an authenticated participant drop all limits
|
||||
room._muc_messages_limit = false;
|
||||
end
|
||||
|
||||
if room._muc_messages_limit == false then
|
||||
-- no limits for this room, just skip
|
||||
return;
|
||||
end
|
||||
|
||||
if not room._muc_messages_limit_count then
|
||||
room._muc_messages_limit_count = 0;
|
||||
end
|
||||
|
||||
room._muc_messages_limit_count = room._muc_messages_limit_count + 1;
|
||||
|
||||
-- on the first message above the limit we set the limit and we send an announcement to the room
|
||||
if room._muc_messages_limit_count == count + 1 then
|
||||
module:log('warn', 'Room message limit reached: %s', room.jid);
|
||||
|
||||
-- send a message to the room
|
||||
local announcement = st.message({ from = room.jid, type = 'groupchat', id = id.medium(), })
|
||||
:tag('body'):text(error_text);
|
||||
room:broadcast_message(announcement);
|
||||
|
||||
room._muc_messages_limit = true;
|
||||
end
|
||||
|
||||
if room._muc_messages_limit == true then
|
||||
-- return error to the sender of this message
|
||||
event.origin.send(st.error_reply(stanza, 'cancel', 'not-allowed', error_text));
|
||||
return true;
|
||||
end
|
||||
end
|
||||
|
||||
-- handle messages sent in the component
|
||||
-- 'message/host' is used for breakout rooms
|
||||
module:hook('message/full', on_message); -- private messages
|
||||
module:hook('message/bare', on_message); -- room messages
|
||||
|
||||
module:hook_global('config-reloaded', load_config);
|
660
roles/jitsi/files/prosody/modules/mod_muc_lobby_rooms.lua
Normal file
660
roles/jitsi/files/prosody/modules/mod_muc_lobby_rooms.lua
Normal file
@ -0,0 +1,660 @@
|
||||
-- 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
|
||||
|
||||
module:depends("jitsi_session");
|
||||
|
||||
local jid_split = require 'util.jid'.split;
|
||||
local jid_bare = require 'util.jid'.bare;
|
||||
local jid_prep = require "util.jid".prep;
|
||||
local jid_resource = require "util.jid".resource;
|
||||
local resourceprep = require "util.encodings".stringprep.resourceprep;
|
||||
local json = require 'cjson.safe';
|
||||
local filters = require 'util.filters';
|
||||
local st = require 'util.stanza';
|
||||
local muc_util = module:require "muc/util";
|
||||
local valid_affiliations = muc_util.valid_affiliations;
|
||||
local MUC_NS = 'http://jabber.org/protocol/muc';
|
||||
local MUC_USER_NS = 'http://jabber.org/protocol/muc#user';
|
||||
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 util = module:require "util";
|
||||
local ends_with = util.ends_with;
|
||||
local get_room_by_name_and_subdomain = util.get_room_by_name_and_subdomain;
|
||||
local is_healthcheck_room = util.is_healthcheck_room;
|
||||
local presence_check_status = util.presence_check_status;
|
||||
local process_host_module = util.process_host_module;
|
||||
|
||||
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;
|
||||
|
||||
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
|
||||
local json_msg_str, error = json.encode(json_msg);
|
||||
|
||||
if not json_msg_str then
|
||||
module:log('error', 'Error broadcasting message room:%s', room.jid, error);
|
||||
return;
|
||||
end
|
||||
|
||||
room:broadcast_message(
|
||||
st.message({ type = 'groupchat', from = occupant.nick })
|
||||
:tag('json-message', {xmlns='http://jitsi.org/jitmeet'})
|
||||
:text(json_msg_str):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 or not lobby_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 not muc_x or presence_check_status(muc_x, '110') then
|
||||
return stanza;
|
||||
end
|
||||
|
||||
local lobby_room_jid = jid_bare(stanza.attr.from);
|
||||
local lobby_room = lobby_muc_service.get_room_from_jid(lobby_room_jid);
|
||||
if not lobby_room then
|
||||
module:log('warn', 'No lobby room found %s', lobby_room_jid);
|
||||
return stanza;
|
||||
end
|
||||
|
||||
-- check is an owner, only owners can receive the presence
|
||||
-- do not forward presence of owners (other than unavailable)
|
||||
local room = main_muc_service.get_room_from_jid(jid_bare(node .. '@' .. main_muc_component_config));
|
||||
local item = muc_x:get_child('item');
|
||||
if not room
|
||||
or stanza.attr.type == 'unavailable'
|
||||
or (room.get_affiliation(room, stanza.attr.to) == 'owner'
|
||||
and room.get_affiliation(room, item.attr.jid) ~= 'owner') then
|
||||
return stanza;
|
||||
end
|
||||
|
||||
local is_to_moderator = lobby_room:get_affiliation(stanza.attr.to) == 'owner';
|
||||
local from_occupant = lobby_room:get_occupant_by_nick(stanza.attr.from);
|
||||
if not from_occupant then
|
||||
if is_to_moderator then
|
||||
return stanza;
|
||||
end
|
||||
|
||||
module:log('warn', 'No lobby occupant found %s', stanza.attr.from);
|
||||
return nil;
|
||||
end
|
||||
|
||||
local from_real_jid;
|
||||
for real_jid in from_occupant:each_session() do
|
||||
from_real_jid = real_jid;
|
||||
end
|
||||
|
||||
if is_to_moderator and lobby_room:get_affiliation(from_real_jid) ~= 'owner' then
|
||||
return stanza;
|
||||
end
|
||||
elseif stanza.name == 'iq' and stanza:get_child('query', DISCO_INFO_NS) then
|
||||
-- allow disco info from the lobby component
|
||||
return stanza;
|
||||
elseif stanza.name == 'message' then
|
||||
-- allow messages to or from moderator
|
||||
local lobby_room_jid = jid_bare(stanza.attr.from);
|
||||
local lobby_room = lobby_muc_service.get_room_from_jid(lobby_room_jid);
|
||||
|
||||
if not lobby_room then
|
||||
module:log('warn', 'No lobby room found %s', stanza.attr.from);
|
||||
return nil;
|
||||
end
|
||||
|
||||
local is_to_moderator = lobby_room:get_affiliation(stanza.attr.to) == 'owner';
|
||||
local from_occupant = lobby_room:get_occupant_by_nick(stanza.attr.from);
|
||||
|
||||
local from_real_jid;
|
||||
if from_occupant then
|
||||
for real_jid in from_occupant:each_session() do
|
||||
from_real_jid = real_jid;
|
||||
end
|
||||
end
|
||||
|
||||
local is_from_moderator = lobby_room:get_affiliation(from_real_jid) == 'owner';
|
||||
|
||||
if is_to_moderator or is_from_moderator then
|
||||
return stanza;
|
||||
end
|
||||
return nil;
|
||||
end
|
||||
|
||||
return nil;
|
||||
else
|
||||
return stanza;
|
||||
end
|
||||
end
|
||||
function filter_session(session)
|
||||
-- domain mapper is filtering on default priority 0, and we need it after that
|
||||
filters.add_filter(session, 'stanzas/out', filter_stanza, -1);
|
||||
end
|
||||
|
||||
-- actor can be null if called from backend (another module using hook create-lobby-room)
|
||||
function attach_lobby_room(room, actor)
|
||||
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("info","Lobby room jid = %s created from:%s", lobby_room_jid, actor);
|
||||
new_room.main_room = room;
|
||||
room._data.lobbyroom = new_room.jid;
|
||||
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 lobby_muc_service and room and room._data.lobbyroom then
|
||||
local lobby_room_obj = lobby_muc_service.get_room_from_jid(room._data.lobbyroom);
|
||||
if lobby_room_obj then
|
||||
lobby_room_obj:set_persistent(false);
|
||||
lobby_room_obj:destroy(newjid, message);
|
||||
end
|
||||
room._data.lobbyroom = nil;
|
||||
end
|
||||
end
|
||||
|
||||
-- This is a copy of the function(handle_admin_query_set_command) from prosody 12 (d7857ef7843a)
|
||||
function handle_admin_query_set_command_item(self, origin, stanza, item)
|
||||
if not item then
|
||||
origin.send(st.error_reply(stanza, "cancel", "bad-request"));
|
||||
return true;
|
||||
end
|
||||
if item.attr.jid then -- Validate provided JID
|
||||
item.attr.jid = jid_prep(item.attr.jid);
|
||||
if not item.attr.jid then
|
||||
origin.send(st.error_reply(stanza, "modify", "jid-malformed"));
|
||||
return true;
|
||||
elseif jid_resource(item.attr.jid) then
|
||||
origin.send(st.error_reply(stanza, "modify", "jid-malformed", "Bare JID expected, got full JID"));
|
||||
return true;
|
||||
end
|
||||
end
|
||||
if item.attr.nick then -- Validate provided nick
|
||||
item.attr.nick = resourceprep(item.attr.nick);
|
||||
if not item.attr.nick then
|
||||
origin.send(st.error_reply(stanza, "modify", "jid-malformed", "invalid nickname"));
|
||||
return true;
|
||||
end
|
||||
end
|
||||
if not item.attr.jid and item.attr.nick then
|
||||
-- COMPAT Workaround for Miranda sending 'nick' instead of 'jid' when changing affiliation
|
||||
local occupant = self:get_occupant_by_nick(self.jid.."/"..item.attr.nick);
|
||||
if occupant then item.attr.jid = occupant.bare_jid; end
|
||||
elseif item.attr.role and not item.attr.nick and item.attr.jid then
|
||||
-- Role changes should use nick, but we have a JID so pull the nick from that
|
||||
local nick = self:get_occupant_jid(item.attr.jid);
|
||||
if nick then item.attr.nick = jid_resource(nick); end
|
||||
end
|
||||
local actor = stanza.attr.from;
|
||||
local reason = item:get_child_text("reason");
|
||||
local success, errtype, err
|
||||
if item.attr.affiliation and item.attr.jid and not item.attr.role then
|
||||
local registration_data;
|
||||
if item.attr.nick then
|
||||
local room_nick = self.jid.."/"..item.attr.nick;
|
||||
local existing_occupant = self:get_occupant_by_nick(room_nick);
|
||||
if existing_occupant and existing_occupant.bare_jid ~= item.attr.jid then
|
||||
module:log("debug", "Existing occupant for %s: %s does not match %s", room_nick, existing_occupant.bare_jid, item.attr.jid);
|
||||
self:set_role(true, room_nick, nil, "This nickname is reserved");
|
||||
end
|
||||
module:log("debug", "Reserving %s for %s (%s)", item.attr.nick, item.attr.jid, item.attr.affiliation);
|
||||
registration_data = { reserved_nickname = item.attr.nick };
|
||||
end
|
||||
success, errtype, err = self:set_affiliation(actor, item.attr.jid, item.attr.affiliation, reason, registration_data);
|
||||
elseif item.attr.role and item.attr.nick and not item.attr.affiliation then
|
||||
success, errtype, err = self:set_role(actor, self.jid.."/"..item.attr.nick, item.attr.role, reason);
|
||||
else
|
||||
success, errtype, err = nil, "cancel", "bad-request";
|
||||
end
|
||||
self:save(true);
|
||||
if not success then
|
||||
origin.send(st.error_reply(stanza, errtype, err));
|
||||
else
|
||||
origin.send(st.reply(stanza));
|
||||
end
|
||||
end
|
||||
|
||||
-- this is extracted from prosody to handle multiple invites
|
||||
function handle_mediated_invite(room, origin, stanza, payload, host_module)
|
||||
local invitee = jid_prep(payload.attr.to);
|
||||
if not invitee then
|
||||
origin.send(st.error_reply(stanza, "cancel", "jid-malformed"));
|
||||
return true;
|
||||
elseif host_module:fire_event("muc-pre-invite", {room = room, origin = origin, stanza = stanza}) then
|
||||
return true;
|
||||
end
|
||||
local invite = muc_util.filter_muc_x(st.clone(stanza));
|
||||
invite.attr.from = room.jid;
|
||||
invite.attr.to = invitee;
|
||||
invite:tag('x', { xmlns = MUC_USER_NS })
|
||||
:tag('invite', {from = stanza.attr.from;})
|
||||
:tag('reason'):text(payload:get_child_text("reason")):up()
|
||||
:up()
|
||||
:up();
|
||||
if not host_module:fire_event("muc-invite", {room = room, stanza = invite, origin = origin, incoming = stanza}) then
|
||||
local join = invite:get_child('x', MUC_USER_NS);
|
||||
-- make sure we filter password added by any module
|
||||
if join then
|
||||
local password = join:get_child('password');
|
||||
if password then
|
||||
join:maptags(
|
||||
function(tag)
|
||||
for k, v in pairs(tag) do
|
||||
if k == 'name' and v == 'password' then
|
||||
return nil
|
||||
end
|
||||
end
|
||||
return tag
|
||||
end
|
||||
);
|
||||
end
|
||||
end
|
||||
room:route_stanza(invite);
|
||||
end
|
||||
return true;
|
||||
end
|
||||
|
||||
local prosody_overrides = {
|
||||
-- handle multiple items at once
|
||||
handle_admin_query_set_command = function(self, origin, stanza)
|
||||
for i=1,#stanza.tags[1] do
|
||||
if handle_admin_query_set_command_item(self, origin, stanza, stanza.tags[1].tags[i]) then
|
||||
return true;
|
||||
end
|
||||
end
|
||||
return true;
|
||||
end,
|
||||
-- this is extracted from prosody to handle multiple invites
|
||||
handle_message_to_room = function(room, origin, stanza, host_module)
|
||||
local type = stanza.attr.type;
|
||||
if type == nil or type == "normal" then
|
||||
local x = stanza:get_child("x", MUC_USER_NS);
|
||||
if x then
|
||||
local handled = false;
|
||||
for _, payload in pairs(x.tags) do
|
||||
if payload ~= nil and payload.name == "invite" and payload.attr.to then
|
||||
handled = true;
|
||||
handle_mediated_invite(room, origin, stanza, payload, host_module)
|
||||
end
|
||||
end
|
||||
return handled;
|
||||
end
|
||||
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 check_display_name_required then
|
||||
local room = get_room_by_name_and_subdomain(session.jitsi_web_query_room, session.jitsi_web_query_prefix);
|
||||
|
||||
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 presence_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, actor);
|
||||
if lobby_created then
|
||||
module:fire_event('jitsi-lobby-enabled', { room = room; });
|
||||
event.status_codes['104'] = true;
|
||||
notify_lobby_enabled(room, actor, true);
|
||||
end
|
||||
elseif room._data.lobbyroom then
|
||||
destroy_lobby_room(room, room.jid);
|
||||
module:fire_event('jitsi-lobby-disabled', { room = room; });
|
||||
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;
|
||||
end
|
||||
end);
|
||||
|
||||
host_module:hook('muc-occupant-pre-join', function (event)
|
||||
local occupant, room, stanza = event.occupant, event.room, event.stanza;
|
||||
|
||||
if is_healthcheck_room(room.jid) or not room:get_members_only() or ends_with(occupant.nick, '/focus') 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 it was already set to be whitelisted member
|
||||
if not affiliation or affiliation == 'none' or affiliation == 'member' then
|
||||
occupant.role = 'participant';
|
||||
room:set_affiliation(true, invitee_bare_jid, 'member');
|
||||
room:save_occupant(occupant);
|
||||
|
||||
return;
|
||||
end
|
||||
elseif room:get_password() then
|
||||
local affiliation = room:get_affiliation(invitee);
|
||||
-- if pre-approved and password is set for the room, add the password to allow joining
|
||||
if affiliation == 'member' and not password then
|
||||
join:tag('password', { xmlns = MUC_NS }):text(room:get_password());
|
||||
end
|
||||
end
|
||||
|
||||
-- Check for display name if missing return an error
|
||||
local displayName = stanza:get_child_text('nick', 'http://jabber.org/protocol/nick');
|
||||
if (not displayName or #displayName == 0) and not room._data.lobby_skip_display_name_check then
|
||||
local reply = st.error_reply(stanza, 'modify', 'not-acceptable');
|
||||
reply.tags[1].attr.code = '406';
|
||||
reply:tag('displayname-required', { xmlns = 'http://jitsi.org/jitmeet', lobby = 'true' }):up():up();
|
||||
|
||||
event.origin.send(reply:tag('x', {xmlns = MUC_NS}));
|
||||
return true;
|
||||
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');
|
||||
reply.tags[1].attr.code = '407';
|
||||
if room._data.lobby_extra_reason then
|
||||
reply:tag(room._data.lobby_extra_reason, { xmlns = 'http://jitsi.org/jitmeet' }):up();
|
||||
end
|
||||
reply:tag('lobbyroom', { xmlns = 'http://jitsi.org/jitmeet' }):text(room._data.lobbyroom):up():up();
|
||||
|
||||
-- TODO: Drop this tag at some point (when all mobile clients and jigasi are updated), as this violates the rfc
|
||||
reply:tag('lobbyroom'):text(room._data.lobbyroom):up();
|
||||
|
||||
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', MUC_USER_NS)
|
||||
:get_child('invite').attr.from;
|
||||
|
||||
if lobby_muc_service and room._data.lobbyroom then
|
||||
local lobby_room_obj = lobby_muc_service.get_room_from_jid(room._data.lobbyroom);
|
||||
if lobby_room_obj then
|
||||
local occupant = lobby_room_obj: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);
|
||||
|
||||
-- listen for admin set
|
||||
for event_name, method in pairs {
|
||||
-- Normal room interactions
|
||||
["iq-set/bare/http://jabber.org/protocol/muc#admin:query"] = "handle_admin_query_set_command" ;
|
||||
["message/bare"] = "handle_message_to_room" ;
|
||||
-- Host room
|
||||
["iq-set/host/http://jabber.org/protocol/muc#admin:query"] = "handle_admin_query_set_command" ;
|
||||
["message/host"] = "handle_message_to_room" ;
|
||||
} do
|
||||
host_module:hook(event_name, function (event)
|
||||
local origin, stanza = event.origin, event.stanza;
|
||||
local room_jid = jid_bare(stanza.attr.to);
|
||||
local room = get_room_from_jid(room_jid);
|
||||
|
||||
if room then
|
||||
return prosody_overrides[method](room, origin, stanza, host_module);
|
||||
end
|
||||
end, 1) -- make sure we handle it before prosody that uses priority -2 for this
|
||||
end
|
||||
end);
|
||||
|
||||
function handle_create_lobby(event)
|
||||
local room = event.room;
|
||||
|
||||
-- since this is called by backend rather than triggered by UI, we need to handle a few additional things:
|
||||
-- 1. Make sure existing participants are already members or they will get kicked out when set_members_only(true)
|
||||
-- 2. Trigger a 104 (config change) status message so UI state is properly updated for existing users
|
||||
|
||||
-- make sure all existing occupants are members
|
||||
for _, occupant in room:each_occupant() do
|
||||
local affiliation = room:get_affiliation(occupant.bare_jid);
|
||||
if valid_affiliations[affiliation or "none"] < valid_affiliations.member then
|
||||
room:set_affiliation(true, occupant.bare_jid, 'member');
|
||||
end
|
||||
end
|
||||
-- Now it is safe to set the room to members only
|
||||
room:set_members_only(true);
|
||||
room._data.lobby_extra_reason = event.reason;
|
||||
room._data.lobby_skip_display_name_check = event.skip_display_name_check;
|
||||
|
||||
-- Trigger a presence with 104 so existing participants retrieves new muc#roomconfig
|
||||
room:broadcast_message(
|
||||
st.message({ type='groupchat', from=room.jid })
|
||||
:tag('x', { xmlns = MUC_USER_NS })
|
||||
:tag('status', { code='104' })
|
||||
);
|
||||
|
||||
-- Attach the lobby room.
|
||||
attach_lobby_room(room);
|
||||
end
|
||||
|
||||
function handle_destroy_lobby(event)
|
||||
local room = event.room;
|
||||
|
||||
-- since this is called by backend rather than triggered by UI, we need to
|
||||
-- trigger a 104 (config change) status message so UI state is properly updated for existing users (and jicofo)
|
||||
destroy_lobby_room(room, event.newjid, event.message);
|
||||
|
||||
-- Trigger a presence with 104 so existing participants retrieves new muc#roomconfig
|
||||
room:broadcast_message(
|
||||
st.message({ type='groupchat', from=room.jid })
|
||||
:tag('x', { xmlns = MUC_USER_NS })
|
||||
:tag('status', { code='104' })
|
||||
);
|
||||
end
|
||||
|
||||
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);
|
72
roles/jitsi/files/prosody/modules/mod_muc_max_occupants.lua
Normal file
72
roles/jitsi/files/prosody/modules/mod_muc_max_occupants.lua
Normal file
@ -0,0 +1,72 @@
|
||||
-- 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 is_healthcheck_room = module:require "util".is_healthcheck_room;
|
||||
|
||||
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 user, domain, res = split_jid(stanza.attr.from);
|
||||
|
||||
if is_healthcheck_room(room.jid) then
|
||||
return;
|
||||
end
|
||||
|
||||
--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 max_occupants_by_room = event.room._data.max_occupants;
|
||||
local count = count_keys(room._occupants);
|
||||
-- if no of occupants limit is set per room basis use
|
||||
-- that settings otherwise use the global one
|
||||
local slots = max_occupants_by_room or MAX_OCCUPANTS;
|
||||
|
||||
-- If there is no whitelist, just check the count.
|
||||
if not whitelist and count >= slots 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 or (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
|
133
roles/jitsi/files/prosody/modules/mod_muc_meeting_id.lua
Normal file
133
roles/jitsi/files/prosody/modules/mod_muc_meeting_id.lua
Normal file
@ -0,0 +1,133 @@
|
||||
local queue = require "util.queue";
|
||||
local uuid_gen = require "util.uuid".generate;
|
||||
local main_util = module:require "util";
|
||||
local ends_with = main_util.ends_with;
|
||||
local is_healthcheck_room = main_util.is_healthcheck_room;
|
||||
local internal_room_jid_match_rewrite = main_util.internal_room_jid_match_rewrite;
|
||||
local presence_check_status = main_util.presence_check_status;
|
||||
|
||||
local um_is_admin = require 'core.usermanager'.is_admin;
|
||||
local function is_admin(jid)
|
||||
return um_is_admin(jid, module.host);
|
||||
end
|
||||
|
||||
local QUEUE_MAX_SIZE = 500;
|
||||
|
||||
-- Module that generates a unique meetingId, attaches it to the room
|
||||
-- and adds it to all disco info form data (when room is queried or in the
|
||||
-- initial room owner config)
|
||||
|
||||
-- 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);
|
||||
|
||||
-- disabled few options for room config, to not mess with visitor logic
|
||||
module:hook("muc-config-submitted/muc#roomconfig_moderatedroom", function()
|
||||
return true;
|
||||
end, 99);
|
||||
module:hook("muc-config-submitted/muc#roomconfig_presencebroadcast", function()
|
||||
return true;
|
||||
end, 99);
|
||||
module:hook("muc-config-submitted/muc#roominfo_meetingId", function(event)
|
||||
-- we allow jicofo to overwrite the meetingId
|
||||
if is_admin(event.actor) then
|
||||
event.room._data.meetingId = event.value;
|
||||
return;
|
||||
end
|
||||
|
||||
return true;
|
||||
end, 99);
|
||||
module:hook('muc-broadcast-presence', function (event)
|
||||
local actor, occupant, room, x = event.actor, event.occupant, event.room, event.x;
|
||||
if presence_check_status(x, '307') then
|
||||
-- make sure we update and affiliation for kicked users
|
||||
room:set_affiliation(actor, occupant.bare_jid, 'none');
|
||||
end
|
||||
end);
|
||||
|
||||
--- Avoids any participant joining the room in the interval between creating the room
|
||||
--- and jicofo entering the room
|
||||
module:hook('muc-occupant-pre-join', function (event)
|
||||
local room, stanza = event.room, event.stanza;
|
||||
|
||||
-- we skip processing only if jicofo_lock is set to false
|
||||
if room._data.jicofo_lock == false or is_healthcheck_room(room.jid) then
|
||||
return;
|
||||
end
|
||||
|
||||
local occupant = event.occupant;
|
||||
if ends_with(occupant.nick, '/focus') then
|
||||
module:fire_event('jicofo-unlock-room', { room = room; });
|
||||
else
|
||||
room._data.jicofo_lock = true;
|
||||
if not room.pre_join_queue then
|
||||
room.pre_join_queue = queue.new(QUEUE_MAX_SIZE);
|
||||
end
|
||||
|
||||
if not room.pre_join_queue:push(event) then
|
||||
module:log('error', 'Error enqueuing occupant event for: %s', occupant.nick);
|
||||
return true;
|
||||
end
|
||||
module:log('debug', 'Occupant pushed to prejoin queue %s', occupant.nick);
|
||||
|
||||
-- stop processing
|
||||
return true;
|
||||
end
|
||||
end, 8); -- just after the rate limit
|
||||
|
||||
function handle_jicofo_unlock(event)
|
||||
local room = event.room;
|
||||
|
||||
room._data.jicofo_lock = false;
|
||||
if not room.pre_join_queue then
|
||||
return;
|
||||
end
|
||||
|
||||
-- and now let's handle all pre_join_queue events
|
||||
for _, ev in room.pre_join_queue:items() do
|
||||
-- if the connection was closed while waiting in the queue, ignore
|
||||
if ev.origin.conn then
|
||||
module:log('debug', 'Occupant processed from queue %s', ev.occupant.nick);
|
||||
room:handle_normal_presence(ev.origin, ev.stanza);
|
||||
end
|
||||
end
|
||||
room.pre_join_queue = nil;
|
||||
end
|
||||
|
||||
module:hook('jicofo-unlock-room', handle_jicofo_unlock);
|
||||
|
||||
-- make sure we remove nick if someone is sending it with a message to protect
|
||||
-- forgery of display name
|
||||
module:hook("muc-occupant-groupchat", function(event)
|
||||
event.stanza:remove_children('nick', 'http://jabber.org/protocol/nick');
|
||||
end, 45); -- prosody check is prio 50, we want to run after it
|
186
roles/jitsi/files/prosody/modules/mod_muc_password_check.lua
Normal file
186
roles/jitsi/files/prosody/modules/mod_muc_password_check.lua
Normal file
@ -0,0 +1,186 @@
|
||||
local inspect = require "inspect";
|
||||
local formdecode = require "util.http".formdecode;
|
||||
local urlencode = require "util.http".urlencode;
|
||||
local jid = require "util.jid";
|
||||
local json = require 'cjson.safe';
|
||||
local util = module:require "util";
|
||||
local async_handler_wrapper = util.async_handler_wrapper;
|
||||
local starts_with = util.starts_with;
|
||||
local process_host_module = util.process_host_module;
|
||||
local token_util = module:require "token/util".new(module);
|
||||
|
||||
-- option to enable/disable room API token verifications
|
||||
local enableTokenVerification
|
||||
= module:get_option_boolean("enable_password_token_verification", true);
|
||||
|
||||
local muc_domain_base = module:get_option_string("muc_mapper_domain_base");
|
||||
if not muc_domain_base then
|
||||
module:log("warn", "No 'muc_domain_base' option set, disabling password check endpoint.");
|
||||
return ;
|
||||
end
|
||||
local muc_domain_prefix = module:get_option_string("muc_mapper_domain_prefix", "conference");
|
||||
|
||||
local json_content_type = "application/json";
|
||||
|
||||
--- Verifies the token
|
||||
-- @param token the token we received
|
||||
-- @param room_address the full room address jid
|
||||
-- @return true if values are ok or false otherwise
|
||||
function verify_token(token, room_address)
|
||||
if 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
|
||||
module:log("warn", "no token provided for %s", room_address);
|
||||
return false;
|
||||
end
|
||||
|
||||
local session = {};
|
||||
session.auth_token = token;
|
||||
local verified, reason, msg = token_util:process_and_verify_token(session);
|
||||
if not verified then
|
||||
module:log("warn", "not a valid token %s %s for %s", tostring(reason), tostring(msg), room_address);
|
||||
return false;
|
||||
end
|
||||
|
||||
return true;
|
||||
end
|
||||
|
||||
-- Validates the request by checking for required url param room and
|
||||
-- validates the token provided with the request
|
||||
-- @param request - The request to validate.
|
||||
-- @return [error_code, room]
|
||||
local function validate_and_get_room(request)
|
||||
if not request.url.query then
|
||||
module:log("warn", "No query");
|
||||
return 400, nil;
|
||||
end
|
||||
|
||||
local params = formdecode(request.url.query);
|
||||
local room_name = urlencode(params.room) or "";
|
||||
local subdomain = urlencode(params.prefix) or "";
|
||||
|
||||
if not room_name then
|
||||
module:log("warn", "Missing room param for %s", room_name);
|
||||
return 400, nil;
|
||||
end
|
||||
|
||||
local room_address = jid.join(room_name, muc_domain_prefix.."."..muc_domain_base);
|
||||
|
||||
if subdomain and subdomain ~= "" then
|
||||
room_address = "["..subdomain.."]"..room_address;
|
||||
end
|
||||
|
||||
-- verify access
|
||||
local token = request.headers["authorization"]
|
||||
|
||||
if token and starts_with(token,'Bearer ') then
|
||||
token = token:sub(8,#token)
|
||||
end
|
||||
|
||||
if not verify_token(token, room_address) then
|
||||
return 403, nil;
|
||||
end
|
||||
|
||||
local room = get_room_from_jid(room_address);
|
||||
|
||||
if not room then
|
||||
module:log("warn", "No room found for %s", room_address);
|
||||
return 404, nil;
|
||||
else
|
||||
return 200, room;
|
||||
end
|
||||
end
|
||||
|
||||
function handle_validate_room_password (event)
|
||||
local request = event.request;
|
||||
|
||||
if request.headers.content_type ~= json_content_type
|
||||
or (not request.body or #request.body == 0) then
|
||||
module:log("warn", "Wrong content type: %s", request.headers.content_type);
|
||||
return { status_code = 400; }
|
||||
end
|
||||
|
||||
local params, error = json.decode(request.body);
|
||||
if not params then
|
||||
module:log("warn", "Missing params error:%s", error);
|
||||
return { status_code = 400; }
|
||||
end
|
||||
|
||||
local passcode = params["passcode"];
|
||||
|
||||
if not passcode then
|
||||
module:log("warn", "Missing passcode param");
|
||||
return { status_code = 400; };
|
||||
end
|
||||
|
||||
local error_code, room = validate_and_get_room(request);
|
||||
|
||||
if not room then
|
||||
return { status_code = error_code; }
|
||||
end
|
||||
|
||||
local json_msg_str, error_encode = json.encode({ valid = (room:get_password() == passcode) });
|
||||
if not json_msg_str then
|
||||
module:log('error', 'Cannot encode json room:%s error:%s', room.jid, error_encode);
|
||||
return { status_code = 400; };
|
||||
end
|
||||
|
||||
local PUT_response = {
|
||||
headers = { content_type = "application/json"; };
|
||||
body = json_msg_str;
|
||||
};
|
||||
|
||||
-- module:log("debug","Sending response for room password validate: %s", inspect(PUT_response));
|
||||
|
||||
return PUT_response;
|
||||
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_password (event)
|
||||
local error_code, room = validate_and_get_room(event.request);
|
||||
|
||||
if not room then
|
||||
return { status_code = error_code; }
|
||||
end
|
||||
|
||||
room_details = {};
|
||||
room_details["conference"] = room.jid;
|
||||
room_details["passcodeProtected"] = room:get_password() ~= nil;
|
||||
room_details["lobbyEnabled"] = room._data ~= nil and room._data.lobbyroom ~= nil;
|
||||
|
||||
local json_msg_str, error = json.encode(room_details);
|
||||
if not json_msg_str then
|
||||
module:log('error', 'Cannot encode json room:%s error:%s', room.jid, error);
|
||||
return { status_code = 400; };
|
||||
end
|
||||
|
||||
local GET_response = {
|
||||
headers = {
|
||||
content_type = "application/json";
|
||||
};
|
||||
body = json_msg_str;
|
||||
};
|
||||
-- module:log("debug","Sending response for room password: %s", inspect(GET_response));
|
||||
|
||||
return GET_response;
|
||||
end
|
||||
|
||||
process_host_module(muc_domain_base, function(host_module, host)
|
||||
module:log("info","Adding http handler for /room-info on %s", host_module.host);
|
||||
host_module:depends("http");
|
||||
host_module:provides("http", {
|
||||
default_path = "/";
|
||||
route = {
|
||||
["GET room-info"] = function (event) return async_handler_wrapper(event, handle_get_room_password) end;
|
||||
["PUT room-info"] = function (event) return async_handler_wrapper(event, handle_validate_room_password) end;
|
||||
};
|
||||
});
|
||||
end);
|
@ -0,0 +1,56 @@
|
||||
--- AUTHOR: https://gist.github.com/legastero Lance Stout
|
||||
local jid_split = require "util.jid".split;
|
||||
local whitelist = module:get_option_set("muc_password_whitelist");
|
||||
|
||||
local MUC_NS = "http://jabber.org/protocol/muc";
|
||||
|
||||
|
||||
module:hook("muc-occupant-pre-join", function (event)
|
||||
local room, stanza = event.room, event.stanza;
|
||||
|
||||
local user, domain, res = jid_split(event.stanza.attr.from);
|
||||
|
||||
--no user object means no way to check whitelist
|
||||
if user == nil then
|
||||
return
|
||||
end
|
||||
|
||||
if not whitelist then
|
||||
return;
|
||||
end
|
||||
if not whitelist:contains(domain) and not whitelist:contains(user..'@'..domain) then
|
||||
return;
|
||||
end
|
||||
|
||||
local join = stanza:get_child("x", MUC_NS);
|
||||
if not join then
|
||||
join = stanza:tag("x", { xmlns = MUC_NS });
|
||||
end
|
||||
|
||||
local password = join:get_child("password", MUC_NS);
|
||||
if password then
|
||||
-- removes <password... node,
|
||||
-- seems like password:text( appends text, not replacing it
|
||||
join:maptags(
|
||||
function(tag)
|
||||
for k, v in pairs(tag) do
|
||||
if k == "name" and v == "password" then
|
||||
return nil
|
||||
end
|
||||
end
|
||||
return tag
|
||||
end
|
||||
);
|
||||
|
||||
end
|
||||
|
||||
join:tag("password", { xmlns = MUC_NS }):text(room:get_password());
|
||||
|
||||
-- module:log("debug", "Applied password access whitelist for %s in room %s", event.stanza.attr.from, room.jid);
|
||||
end, -7); --- Run before the password check (priority -20), runs after lobby(priority -4) and members-only (priority -5).
|
||||
|
||||
|
||||
module:hook_global("config-reloaded", function (event)
|
||||
module:log("debug", "Reloading MUC password access whitelist");
|
||||
whitelist = module:get_option_set("muc_password_whitelist");
|
||||
end)
|
319
roles/jitsi/files/prosody/modules/mod_muc_poltergeist.lua
Normal file
319
roles/jitsi/files/prosody/modules/mod_muc_poltergeist.lua
Normal file
@ -0,0 +1,319 @@
|
||||
local bare = require "util.jid".bare;
|
||||
local get_room_by_name_and_subdomain = module:require "util".get_room_by_name_and_subdomain;
|
||||
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
|
||||
|
||||
module:depends("jitsi_session");
|
||||
|
||||
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
|
||||
|
||||
--- 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_by_name_and_subdomain(
|
||||
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_by_name_and_subdomain(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_by_name_and_subdomain(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_by_name_and_subdomain(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;
|
||||
};
|
||||
});
|
231
roles/jitsi/files/prosody/modules/mod_muc_rate_limit.lua
Normal file
231
roles/jitsi/files/prosody/modules/mod_muc_rate_limit.lua
Normal file
@ -0,0 +1,231 @@
|
||||
-- enable under the main muc component
|
||||
|
||||
local queue = require "util.queue";
|
||||
local new_throttle = require "util.throttle".create;
|
||||
local timer = require "util.timer";
|
||||
local st = require "util.stanza";
|
||||
|
||||
-- we max to 500 participants per meeting so this should be enough, we are not suppose to handle all
|
||||
-- participants in one meeting
|
||||
local PRESENCE_QUEUE_MAX_SIZE = 1000;
|
||||
|
||||
-- default to 3 participants per second
|
||||
local join_rate_per_conference = module:get_option_number("muc_rate_joins", 3);
|
||||
local leave_rate_per_conference = module:get_option_number("muc_rate_leaves", 5);
|
||||
|
||||
-- Measure/monitor the room rate limiting queue
|
||||
local measure = require "core.statsmanager".measure;
|
||||
local measure_longest_queue = measure("distribution",
|
||||
"/mod_" .. module.name .. "/longest_queue");
|
||||
local measure_rooms_with_queue = measure("rate",
|
||||
"/mod_" .. module.name .. "/rooms_with_queue");
|
||||
|
||||
-- throws a stat that the queue was full, counts the total number of times we hit it
|
||||
local measure_full_queue = measure("rate",
|
||||
"/mod_" .. module.name .. "/full_queue");
|
||||
|
||||
-- keeps track of the total times we had an error processing the queue
|
||||
local measure_errors_processing_queue = measure("rate",
|
||||
"/mod_" .. module.name .. "/errors_processing_queue");
|
||||
|
||||
-- we keep track here what was the longest queue we have seen
|
||||
local stat_longest_queue = 0;
|
||||
|
||||
-- Adds item to the queue
|
||||
-- @returns false if queue is full and item was not added, true otherwise
|
||||
local function add_item_to_queue(queue, item, room, from, send_stats)
|
||||
if not queue:push(item) then
|
||||
module:log('error',
|
||||
'Error pushing item in %s queue for %s in %s', send_stats and 'join' or 'leave', from, room.jid);
|
||||
|
||||
if send_stats then
|
||||
measure_full_queue();
|
||||
end
|
||||
|
||||
return false;
|
||||
else
|
||||
-- check is this the longest queue and if so throws a stat
|
||||
if send_stats and queue:count() > stat_longest_queue then
|
||||
stat_longest_queue = queue:count();
|
||||
measure_longest_queue(stat_longest_queue);
|
||||
end
|
||||
|
||||
return true;
|
||||
end
|
||||
end
|
||||
|
||||
-- process join_rate_presence_queue in the room and pops element passing them to handle_normal_presence
|
||||
-- returns 1 if we want to reschedule it after 1 second
|
||||
local function timer_process_queue_elements (rate, queue, process, queue_empty_cb)
|
||||
if not queue or queue:count() == 0 or queue.empty then
|
||||
return;
|
||||
end
|
||||
|
||||
for _ = 1, rate do
|
||||
local ev = queue:pop();
|
||||
if ev then
|
||||
process(ev);
|
||||
end
|
||||
end
|
||||
|
||||
-- if there are elements left, schedule an execution in a second
|
||||
if queue:count() > 0 then
|
||||
return 1;
|
||||
else
|
||||
queue_empty_cb();
|
||||
end
|
||||
end
|
||||
|
||||
-- we check join rate before occupant joins. If rate is exceeded we queue the events and start a timer
|
||||
-- that will run every second processing the events passing them to the room handling function handle_normal_presence
|
||||
-- from where those arrived, this way we keep a maximum rate of joining
|
||||
module:hook("muc-occupant-pre-join", function (event)
|
||||
local room, stanza = event.room, event.stanza;
|
||||
|
||||
-- skipping events we had produced and clear our flag
|
||||
if stanza.delayed_join_skip == true then
|
||||
event.stanza.delayed_join_skip = nil;
|
||||
return nil;
|
||||
end
|
||||
|
||||
local throttle = room.join_rate_throttle;
|
||||
if not room.join_rate_throttle then
|
||||
throttle = new_throttle(join_rate_per_conference, 1); -- rate per one second
|
||||
room.join_rate_throttle = throttle;
|
||||
end
|
||||
|
||||
if not throttle:poll(1) then
|
||||
if not room.join_rate_presence_queue then
|
||||
-- if this is the first item for a room we increment the stat for rooms with queues
|
||||
measure_rooms_with_queue();
|
||||
room.join_rate_presence_queue = queue.new(PRESENCE_QUEUE_MAX_SIZE);
|
||||
end
|
||||
|
||||
if not add_item_to_queue(room.join_rate_presence_queue, event, room, stanza.attr.from, true) then
|
||||
-- let's not stop processing the event
|
||||
return nil;
|
||||
end
|
||||
|
||||
if not room.join_rate_queue_timer then
|
||||
timer.add_task(1, function ()
|
||||
local status, result = pcall(timer_process_queue_elements,
|
||||
join_rate_per_conference,
|
||||
room.join_rate_presence_queue,
|
||||
function(ev)
|
||||
-- we mark what we pass here so we can skip it on the next muc-occupant-pre-join event
|
||||
ev.stanza.delayed_join_skip = true;
|
||||
room:handle_normal_presence(ev.origin, ev.stanza);
|
||||
end,
|
||||
function() -- empty callback
|
||||
room.join_rate_queue_timer = false;
|
||||
end
|
||||
);
|
||||
if not status then
|
||||
-- there was an error in the timer function
|
||||
module:log('error', 'Error processing join queue: %s', result);
|
||||
|
||||
measure_errors_processing_queue();
|
||||
|
||||
-- let's re-schedule timer so we do not lose the queue
|
||||
return 1;
|
||||
end
|
||||
|
||||
return result;
|
||||
end);
|
||||
room.join_rate_queue_timer = true;
|
||||
end
|
||||
|
||||
return true; -- we stop execution, so we do not process this join at the moment
|
||||
end
|
||||
|
||||
if room.join_rate_queue_timer then
|
||||
-- there is timer so we need to order the presences, put it in the queue
|
||||
|
||||
-- if add fails as queue is full we return false and the event will continue processing, we risk re-order
|
||||
-- but not losing it
|
||||
return add_item_to_queue(room.join_rate_presence_queue, event, room, stanza.attr.from, true);
|
||||
end
|
||||
|
||||
end, 9); -- as we will rate limit joins we need to be the first to execute
|
||||
-- we ran it after muc_max_occupants which is with priority 10, there is nothing to rate limit
|
||||
-- if max number of occupants is reached
|
||||
|
||||
-- clear queue on room destroy so timer will skip next run if any
|
||||
module:hook('muc-room-destroyed',function(event)
|
||||
if event.room.join_rate_presence_queue then
|
||||
event.room.join_rate_presence_queue.empty = true;
|
||||
end
|
||||
if event.room.leave_rate_presence_queue then
|
||||
event.room.leave_rate_presence_queue.empty = true;
|
||||
end
|
||||
end);
|
||||
|
||||
module:hook('muc-occupant-pre-leave', function (event)
|
||||
local occupant, room, stanza = event.occupant, event.room, event.stanza;
|
||||
local throttle = room.leave_rate_throttle;
|
||||
|
||||
if not throttle then
|
||||
throttle = new_throttle(leave_rate_per_conference, 1); -- rate per one second
|
||||
room.leave_rate_throttle = throttle;
|
||||
end
|
||||
|
||||
if not throttle:poll(1) then
|
||||
if not room.leave_rate_presence_queue then
|
||||
room.leave_rate_presence_queue = queue.new(PRESENCE_QUEUE_MAX_SIZE);
|
||||
end
|
||||
|
||||
-- we need it later when processing the event
|
||||
event.orig_role = occupant.role;
|
||||
|
||||
if not add_item_to_queue(room.leave_rate_presence_queue, event, room, stanza.attr.from, false) then
|
||||
-- let's not stop processing the event
|
||||
return nil;
|
||||
end
|
||||
|
||||
-- set role to nil so the occupant will be removed from room occupants when we save it
|
||||
-- we remove occupant from the list early on batches so we can spare sending few presences
|
||||
occupant.role = nil;
|
||||
room:save_occupant(occupant);
|
||||
|
||||
if not room.leave_rate_queue_timer then
|
||||
timer.add_task(1, function ()
|
||||
local status, result = pcall(timer_process_queue_elements,
|
||||
leave_rate_per_conference,
|
||||
room.leave_rate_presence_queue,
|
||||
function(ev)
|
||||
local occupant, orig_role, origin, room, stanza
|
||||
= ev.occupant, ev.orig_role, ev.origin, ev.room, ev.stanza;
|
||||
|
||||
room:publicise_occupant_status(
|
||||
occupant,
|
||||
st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user";}),
|
||||
nil, nil, nil, orig_role);
|
||||
|
||||
module:fire_event("muc-occupant-left", {
|
||||
room = room;
|
||||
nick = occupant.nick;
|
||||
occupant = occupant;
|
||||
origin = origin;
|
||||
stanza = stanza;
|
||||
});
|
||||
end,
|
||||
function() -- empty callback
|
||||
room.leave_rate_queue_timer = false;
|
||||
end
|
||||
);
|
||||
if not status then
|
||||
-- there was an error in the timer function
|
||||
module:log('error', 'Error processing leave queue: %s', result);
|
||||
|
||||
-- let's re-schedule timer so we do not lose the queue
|
||||
return 1;
|
||||
end
|
||||
|
||||
return result;
|
||||
end);
|
||||
room.leave_rate_queue_timer = true;
|
||||
end
|
||||
|
||||
return true; -- we stop execution, so we do not process this leave at the moment
|
||||
end
|
||||
end);
|
197
roles/jitsi/files/prosody/modules/mod_muc_size.lua
Normal file
197
roles/jitsi/files/prosody/modules/mod_muc_size.lua
Normal file
@ -0,0 +1,197 @@
|
||||
-- Prosody IM
|
||||
-- Copyright (C) 2021-present 8x8, Inc.
|
||||
--
|
||||
|
||||
local jid = require "util.jid";
|
||||
local it = require "util.iterators";
|
||||
local json = require 'cjson.safe';
|
||||
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,
|
||||
-- the 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.jitsi_meet_context_features,'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!");
|
102
roles/jitsi/files/prosody/modules/mod_muc_wait_for_host.lua
Normal file
102
roles/jitsi/files/prosody/modules/mod_muc_wait_for_host.lua
Normal file
@ -0,0 +1,102 @@
|
||||
-- This module is activated under the main muc component
|
||||
-- This will prevent anyone joining the call till jicofo and one moderator join the room
|
||||
-- for the rest of the participants lobby will be turned on and they will be waiting there till
|
||||
-- the main participant joins and lobby will be turned off at that time and rest of the participants will
|
||||
-- join the room. It expects main virtual host to be set to require jwt tokens and guests to use
|
||||
-- the guest domain which is anonymous.
|
||||
-- The module has the option to set participants to moderators when connected via token/when they are authenticated
|
||||
-- This module depends on mod_persistent_lobby.
|
||||
local um_is_admin = require 'core.usermanager'.is_admin;
|
||||
local jid = require 'util.jid';
|
||||
local util = module:require "util";
|
||||
local is_healthcheck_room = util.is_healthcheck_room;
|
||||
local is_moderated = util.is_moderated;
|
||||
local process_host_module = util.process_host_module;
|
||||
|
||||
local disable_auto_owners = module:get_option_boolean('wait_for_host_disable_auto_owners', false);
|
||||
|
||||
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
|
||||
|
||||
-- to activate this you need the following config in general config file in log = { }
|
||||
-- { to = 'file', filename = '/var/log/prosody/prosody.audit.log', levels = { 'audit' } }
|
||||
local logger = require 'util.logger';
|
||||
local audit_logger = logger.make_logger('mod_'..module.name, 'audit');
|
||||
|
||||
local lobby_muc_component_config = 'lobby.' .. muc_domain_base;
|
||||
local lobby_host;
|
||||
|
||||
if not disable_auto_owners then
|
||||
module:hook('muc-occupant-joined', function (event)
|
||||
local room, occupant, session = event.room, event.occupant, event.origin;
|
||||
local is_moderated_room = is_moderated(room.jid);
|
||||
|
||||
-- for jwt authenticated and username and password authenticated
|
||||
-- only if it is not a moderated room
|
||||
if not is_moderated_room and
|
||||
(session.auth_token or (session.username and jid.host(occupant.bare_jid) == muc_domain_base)) then
|
||||
room:set_affiliation(true, occupant.bare_jid, 'owner');
|
||||
end
|
||||
end, 2);
|
||||
end
|
||||
|
||||
local function is_admin(jid)
|
||||
return um_is_admin(jid, module.host);
|
||||
end
|
||||
|
||||
-- if not authenticated user is trying to join the room we enable lobby in it
|
||||
-- and wait for the moderator to join
|
||||
module:hook('muc-occupant-pre-join', function (event)
|
||||
local room, occupant, session = event.room, event.occupant, event.origin;
|
||||
|
||||
-- we ignore jicofo as we want it to join the room or if the room has already seen its
|
||||
-- authenticated host
|
||||
if is_admin(occupant.bare_jid) or is_healthcheck_room(room.jid) or room.has_host then
|
||||
return;
|
||||
end
|
||||
|
||||
local has_host = false;
|
||||
for _, o in room:each_occupant() do
|
||||
if jid.host(o.bare_jid) == muc_domain_base then
|
||||
room.has_host = true;
|
||||
end
|
||||
end
|
||||
|
||||
if not room.has_host then
|
||||
if session.auth_token or (session.username and jid.host(occupant.bare_jid) == muc_domain_base) then
|
||||
-- the host is here, let's drop the lobby
|
||||
room:set_members_only(false);
|
||||
|
||||
-- let's set the default role of 'participant' for the newly created occupant as it was nil when created
|
||||
-- when the room was still members_only, later if not disabled this participant will become a moderator
|
||||
occupant.role = room:get_default_role(room:get_affiliation(occupant.bare_jid)) or 'participant';
|
||||
|
||||
module:log('info', 'Host %s arrived in %s.', occupant.bare_jid, room.jid);
|
||||
audit_logger('room_jid:%s created_by:%s', room.jid,
|
||||
session.jitsi_meet_context_user and session.jitsi_meet_context_user.id or 'nil');
|
||||
module:fire_event('room_host_arrived', room.jid, session);
|
||||
lobby_host:fire_event('destroy-lobby-room', {
|
||||
room = room,
|
||||
newjid = room.jid,
|
||||
message = 'Host arrived.',
|
||||
});
|
||||
elseif not room:get_members_only() then
|
||||
-- let's enable lobby
|
||||
module:log('info', 'Will wait for host in %s.', room.jid);
|
||||
prosody.events.fire_event('create-persistent-lobby-room', {
|
||||
room = room;
|
||||
reason = 'waiting-for-host',
|
||||
skip_display_name_check = true;
|
||||
});
|
||||
end
|
||||
end
|
||||
end);
|
||||
|
||||
process_host_module(lobby_muc_component_config, function(host_module, host)
|
||||
-- lobby muc component created
|
||||
module:log('info', 'Lobby component loaded %s', host);
|
||||
lobby_host = module:context(host_module);
|
||||
end);
|
199
roles/jitsi/files/prosody/modules/mod_persistent_lobby.lua
Normal file
199
roles/jitsi/files/prosody/modules/mod_persistent_lobby.lua
Normal file
@ -0,0 +1,199 @@
|
||||
-- This module allows lobby room to be created even when the main room is empty.
|
||||
-- Without this module, the empty main room will get deleted after grace period
|
||||
-- which triggers lobby room deletion even if there are still people in the lobby.
|
||||
--
|
||||
-- This module should be added to the main virtual host domain.
|
||||
-- It assumes you have properly configured the muc_lobby_rooms module and lobby muc component.
|
||||
--
|
||||
-- To trigger creation of lobby room:
|
||||
-- prosody.events.fire_event("create-persistent-lobby-room", { room = room; });
|
||||
--
|
||||
module:depends('room_destroy');
|
||||
|
||||
local util = module:require "util";
|
||||
local is_healthcheck_room = util.is_healthcheck_room;
|
||||
local main_muc_component_host = module:get_option_string('main_muc');
|
||||
local lobby_muc_component_host = module:get_option_string('lobby_muc');
|
||||
|
||||
|
||||
if main_muc_component_host == nil then
|
||||
module:log('error', 'main_muc not configured. Cannot proceed.');
|
||||
return;
|
||||
end
|
||||
|
||||
if lobby_muc_component_host == nil then
|
||||
module:log('error', 'lobby not enabled missing lobby_muc config');
|
||||
return;
|
||||
end
|
||||
|
||||
|
||||
-- Helper function to wait till a component is loaded before running the given callback
|
||||
local function run_when_component_loaded(component_host_name, callback)
|
||||
local function trigger_callback()
|
||||
module:log('info', 'Component loaded %s', component_host_name);
|
||||
callback(module:context(component_host_name), component_host_name);
|
||||
end
|
||||
|
||||
if prosody.hosts[component_host_name] == nil then
|
||||
module:log('debug', 'Host %s not yet loaded. Will trigger when it is loaded.', component_host_name);
|
||||
prosody.events.add_handler('host-activated', function (host)
|
||||
if host == component_host_name then
|
||||
trigger_callback();
|
||||
end
|
||||
end);
|
||||
else
|
||||
trigger_callback();
|
||||
end
|
||||
end
|
||||
|
||||
-- Helper function to wait till a component's muc module is loaded before running the given callback
|
||||
local function run_when_muc_module_loaded(component_host_module, component_host_name, callback)
|
||||
local function trigger_callback()
|
||||
module:log('info', 'MUC module loaded for %s', component_host_name);
|
||||
callback(prosody.hosts[component_host_name].modules.muc, component_host_module);
|
||||
end
|
||||
|
||||
if prosody.hosts[component_host_name].modules.muc == nil then
|
||||
module:log('debug', 'MUC module for %s not yet loaded. Will trigger when it is loaded.', component_host_name);
|
||||
prosody.hosts[component_host_name].events.add_handler('module-loaded', function(event)
|
||||
if (event.module == 'muc') then
|
||||
trigger_callback();
|
||||
end
|
||||
end);
|
||||
else
|
||||
trigger_callback()
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local lobby_muc_service;
|
||||
local main_muc_service;
|
||||
local main_muc_module;
|
||||
|
||||
|
||||
-- Helper methods to track rooms that have persistent lobby
|
||||
local function set_persistent_lobby(room)
|
||||
room._data.persist_lobby = true;
|
||||
end
|
||||
|
||||
local function has_persistent_lobby(room)
|
||||
if room._data.persist_lobby == true then
|
||||
return true;
|
||||
else
|
||||
return false;
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- Helper method to trigger main room destroy
|
||||
local function trigger_room_destroy(room)
|
||||
prosody.events.fire_event("maybe-destroy-room", {
|
||||
room = room;
|
||||
reason = 'main room and lobby now empty';
|
||||
caller = module:get_name();
|
||||
});
|
||||
end
|
||||
|
||||
|
||||
-- For rooms with persistent lobby, we need to trigger deletion ourselves when both the main room
|
||||
-- and the lobby room are empty. This will be checked each time an occupant leaves the main room
|
||||
-- of if someone drops off the lobby.
|
||||
|
||||
|
||||
-- Handle events on main muc module
|
||||
run_when_component_loaded(main_muc_component_host, function(host_module, host_name)
|
||||
run_when_muc_module_loaded(host_module, host_name, function (main_muc, main_module)
|
||||
main_muc_service = main_muc; -- so it can be accessed from lobby muc event handlers
|
||||
main_muc_module = main_module;
|
||||
|
||||
main_module:hook("muc-occupant-left", function(event)
|
||||
-- Check if room should be destroyed when someone leaves the main room
|
||||
|
||||
local main_room = event.room;
|
||||
if is_healthcheck_room(main_room.jid) or not has_persistent_lobby(main_room) then
|
||||
return;
|
||||
end
|
||||
|
||||
local lobby_room_jid = main_room._data.lobbyroom;
|
||||
|
||||
-- If occupant leaving results in main room being empty, we trigger room destroy if
|
||||
-- a) lobby exists and is not empty
|
||||
-- b) lobby does not exist (possible for lobby to be disabled manually by moderator in meeting)
|
||||
--
|
||||
-- (main room destroy also triggers lobby room destroy in muc_lobby_rooms)
|
||||
if not main_room:has_occupant() then
|
||||
if lobby_room_jid == nil then -- lobby disabled
|
||||
trigger_room_destroy(main_room);
|
||||
else -- lobby exists
|
||||
local lobby_room = lobby_muc_service.get_room_from_jid(lobby_room_jid);
|
||||
if lobby_room and not lobby_room:has_occupant() then
|
||||
trigger_room_destroy(main_room);
|
||||
end
|
||||
end
|
||||
end
|
||||
end);
|
||||
|
||||
end);
|
||||
end);
|
||||
|
||||
|
||||
-- Handle events on lobby muc module
|
||||
run_when_component_loaded(lobby_muc_component_host, function(host_module, host_name)
|
||||
run_when_muc_module_loaded(host_module, host_name, function (lobby_muc, lobby_module)
|
||||
lobby_muc_service = lobby_muc; -- so it can be accessed from main muc event handlers
|
||||
|
||||
lobby_module:hook("muc-occupant-left", function(event)
|
||||
-- Check if room should be destroyed when someone leaves the lobby
|
||||
|
||||
local lobby_room = event.room;
|
||||
local main_room = lobby_room.main_room;
|
||||
|
||||
if not main_room or is_healthcheck_room(main_room.jid) or not has_persistent_lobby(main_room) then
|
||||
return;
|
||||
end
|
||||
|
||||
-- If both lobby room and main room are empty, we destroy main room.
|
||||
-- (main room destroy also triggers lobby room destroy in muc_lobby_rooms)
|
||||
if not lobby_room:has_occupant() and main_room and not main_room:has_occupant() then
|
||||
trigger_room_destroy(main_room);
|
||||
end
|
||||
|
||||
end);
|
||||
end);
|
||||
end);
|
||||
|
||||
|
||||
function handle_create_persistent_lobby(event)
|
||||
local room = event.room;
|
||||
prosody.events.fire_event("create-lobby-room", event);
|
||||
|
||||
set_persistent_lobby(room);
|
||||
room:set_persistent(true);
|
||||
end
|
||||
|
||||
|
||||
module:hook_global('create-persistent-lobby-room', handle_create_persistent_lobby);
|
||||
|
||||
|
||||
-- Stop other modules from destroying room if persistent lobby not empty
|
||||
function handle_maybe_destroy_main_room(event)
|
||||
local main_room = event.room;
|
||||
local caller = event.caller;
|
||||
|
||||
if caller == module:get_name() then
|
||||
-- we were the one that requested the deletion. Do not override.
|
||||
return nil;
|
||||
end
|
||||
|
||||
-- deletion was requested by another module. Check for lobby occupants.
|
||||
if has_persistent_lobby(main_room) and main_room._data.lobbyroom then
|
||||
local lobby_room_jid = main_room._data.lobbyroom;
|
||||
local lobby_room = lobby_muc_service.get_room_from_jid(lobby_room_jid);
|
||||
if lobby_room and lobby_room:has_occupant() then
|
||||
module:log('info', 'Suppressing room destroy. Persistent lobby still occupied %s', lobby_room_jid);
|
||||
return true; -- stop room destruction
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module:hook_global("maybe-destroy-room", handle_maybe_destroy_main_room);
|
211
roles/jitsi/files/prosody/modules/mod_polls.lua
Normal file
211
roles/jitsi/files/prosody/modules/mod_polls.lua
Normal file
@ -0,0 +1,211 @@
|
||||
-- This module provides persistence for the "polls" feature,
|
||||
-- by keeping track of the state of polls in each room, and sending
|
||||
-- that state to new participants when they join.
|
||||
|
||||
local json = require 'cjson.safe';
|
||||
local st = require("util.stanza");
|
||||
local jid = require "util.jid";
|
||||
local util = module:require("util");
|
||||
local muc = module:depends("muc");
|
||||
|
||||
local NS_NICK = 'http://jabber.org/protocol/nick';
|
||||
local is_healthcheck_room = util.is_healthcheck_room;
|
||||
|
||||
-- Checks if the given stanza contains a JSON message,
|
||||
-- and that the message type pertains to the polls feature.
|
||||
-- If yes, returns the parsed message. Otherwise, returns nil.
|
||||
local function get_poll_message(stanza)
|
||||
if stanza.attr.type ~= "groupchat" then
|
||||
return nil;
|
||||
end
|
||||
local json_data = stanza:get_child_text("json-message", "http://jitsi.org/jitmeet");
|
||||
if json_data == nil then
|
||||
return nil;
|
||||
end
|
||||
local data, error = json.decode(json_data);
|
||||
if not data or (data.type ~= "new-poll" and data.type ~= "answer-poll") then
|
||||
if error then
|
||||
module:log('error', 'Error decoding data error:%s', error);
|
||||
end
|
||||
return nil;
|
||||
end
|
||||
return data;
|
||||
end
|
||||
|
||||
-- Logs a warning and returns true if a room does not
|
||||
-- have poll data associated with it.
|
||||
local function check_polls(room)
|
||||
if room.polls == nil then
|
||||
module:log("warn", "no polls data in room");
|
||||
return true;
|
||||
end
|
||||
return false;
|
||||
end
|
||||
|
||||
--- Returns a table having occupant id and occupant name.
|
||||
--- If the id cannot be extracted from nick a nil value is returned
|
||||
--- if the occupant name cannot be extracted from presence the Fellow Jitster
|
||||
--- name is used
|
||||
local function get_occupant_details(occupant)
|
||||
if not occupant then
|
||||
return nil
|
||||
end
|
||||
local presence = occupant:get_presence();
|
||||
local occupant_name;
|
||||
if presence then
|
||||
occupant_name = presence:get_child("nick", NS_NICK) and presence:get_child("nick", NS_NICK):get_text() or 'Fellow Jitster';
|
||||
else
|
||||
occupant_name = 'Fellow Jitster'
|
||||
end
|
||||
local _, _, occupant_id = jid.split(occupant.nick)
|
||||
if not occupant_id then
|
||||
return nil
|
||||
end
|
||||
return { ["occupant_id"] = occupant_id, ["occupant_name"] = occupant_name }
|
||||
end
|
||||
|
||||
-- Sets up poll data in new rooms.
|
||||
module:hook("muc-room-created", function(event)
|
||||
local room = event.room;
|
||||
if is_healthcheck_room(room.jid) then return end
|
||||
module:log("debug", "setting up polls in room %s", room.jid);
|
||||
room.polls = {
|
||||
by_id = {};
|
||||
order = {};
|
||||
};
|
||||
end);
|
||||
|
||||
-- Keeps track of the current state of the polls in each room,
|
||||
-- by listening to "new-poll" and "answer-poll" messages,
|
||||
-- and updating the room poll data accordingly.
|
||||
-- This mirrors the client-side poll update logic.
|
||||
module:hook("message/bare", function(event)
|
||||
local data = get_poll_message(event.stanza);
|
||||
if data == nil then return end
|
||||
|
||||
local room = muc.get_room_from_jid(event.stanza.attr.to);
|
||||
|
||||
if data.type == "new-poll" then
|
||||
if check_polls(room) then return end
|
||||
|
||||
local occupant_jid = event.stanza.attr.from;
|
||||
local occupant = room:get_occupant_by_real_jid(occupant_jid);
|
||||
if not occupant then
|
||||
module:log("error", "Occupant %s was not found in room %s", occupant_jid, room.jid)
|
||||
return
|
||||
end
|
||||
local poll_creator = get_occupant_details(occupant)
|
||||
if not poll_creator then
|
||||
module:log("error", "Cannot retrieve poll creator id and name for %s from %s", occupant.jid, room.jid)
|
||||
return
|
||||
end
|
||||
|
||||
local answers = {}
|
||||
local compact_answers = {}
|
||||
for i, name in ipairs(data.answers) do
|
||||
table.insert(answers, { name = name, voters = {} });
|
||||
table.insert(compact_answers, { key = i, name = name});
|
||||
end
|
||||
|
||||
local poll = {
|
||||
id = data.pollId,
|
||||
sender_id = poll_creator.occupant_id,
|
||||
sender_name = poll_creator.occupant_name,
|
||||
question = data.question,
|
||||
answers = answers
|
||||
};
|
||||
|
||||
room.polls.by_id[data.pollId] = poll
|
||||
table.insert(room.polls.order, poll)
|
||||
|
||||
local pollData = {
|
||||
event = event,
|
||||
room = room,
|
||||
poll = {
|
||||
pollId = data.pollId,
|
||||
senderId = poll_creator.occupant_id,
|
||||
senderName = poll_creator.occupant_name,
|
||||
question = data.question,
|
||||
answers = compact_answers
|
||||
}
|
||||
}
|
||||
module:fire_event("poll-created", pollData);
|
||||
|
||||
elseif data.type == "answer-poll" then
|
||||
if check_polls(room) then return end
|
||||
|
||||
local occupant_jid = event.stanza.attr.from;
|
||||
local occupant = room:get_occupant_by_real_jid(occupant_jid);
|
||||
if not occupant then
|
||||
module:log("error", "Occupant %s does not exists for room %s", occupant_jid, room.jid)
|
||||
return
|
||||
end
|
||||
local poll = room.polls.by_id[data.pollId];
|
||||
if poll == nil then
|
||||
module:log("warn", "answering inexistent poll");
|
||||
return;
|
||||
end
|
||||
|
||||
local voter = get_occupant_details(occupant)
|
||||
if not voter then
|
||||
module:log("error", "Cannot retrieve voter id and name for %s from %s", occupant.jid, room.jid)
|
||||
return
|
||||
end
|
||||
|
||||
local answers = {};
|
||||
for vote_option_idx, vote_flag in ipairs(data.answers) do
|
||||
table.insert(answers, {
|
||||
key = vote_option_idx,
|
||||
value = vote_flag,
|
||||
name = poll.answers[vote_option_idx].name,
|
||||
});
|
||||
poll.answers[vote_option_idx].voters[voter.occupant_id] = vote_flag and voter.occupant_name or nil;
|
||||
end
|
||||
local answerData = {
|
||||
event = event,
|
||||
room = room,
|
||||
pollId = poll.id,
|
||||
voterName = voter.occupant_name,
|
||||
voterId = voter.occupant_id,
|
||||
answers = answers
|
||||
}
|
||||
module:fire_event("answer-poll", answerData);
|
||||
end
|
||||
end);
|
||||
|
||||
-- Sends the current poll state to new occupants after joining a room.
|
||||
module:hook("muc-occupant-joined", function(event)
|
||||
local room = event.room;
|
||||
if is_healthcheck_room(room.jid) then return end
|
||||
if room.polls == nil or #room.polls.order == 0 then
|
||||
return
|
||||
end
|
||||
|
||||
local data = {
|
||||
type = "old-polls",
|
||||
polls = {},
|
||||
};
|
||||
for i, poll in ipairs(room.polls.order) do
|
||||
data.polls[i] = {
|
||||
id = poll.id,
|
||||
senderId = poll.sender_id,
|
||||
senderName = poll.sender_name,
|
||||
question = poll.question,
|
||||
answers = poll.answers
|
||||
};
|
||||
end
|
||||
|
||||
local json_msg_str, error = json.encode(data);
|
||||
if not json_msg_str then
|
||||
module:log('error', 'Error encoding data room:%s error:%s', room.jid, error);
|
||||
end
|
||||
|
||||
local stanza = st.message({
|
||||
from = room.jid,
|
||||
to = event.occupant.jid
|
||||
})
|
||||
:tag("json-message", { xmlns = "http://jitsi.org/jitmeet" })
|
||||
:text(json_msg_str)
|
||||
:up();
|
||||
room:route_stanza(stanza);
|
||||
end);
|
@ -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);
|
22
roles/jitsi/files/prosody/modules/mod_presence_identity.lua
Normal file
22
roles/jitsi/files/prosody/modules/mod_presence_identity.lua
Normal file
@ -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);
|
242
roles/jitsi/files/prosody/modules/mod_rate_limit.lua
Normal file
242
roles/jitsi/files/prosody/modules/mod_rate_limit.lua
Normal file
@ -0,0 +1,242 @@
|
||||
-- Rate limits connection based on their ip address.
|
||||
-- Rate limits creating sessions (new connections),
|
||||
-- rate limits sent stanzas from same ip address (presence, iq, messages)
|
||||
-- Copyright (C) 2023-present 8x8, Inc.
|
||||
|
||||
local cache = require"util.cache";
|
||||
local ceil = math.ceil;
|
||||
local http_server = require "net.http.server";
|
||||
local gettime = require "util.time".now
|
||||
local filters = require "util.filters";
|
||||
local new_throttle = require "util.throttle".create;
|
||||
local timer = require "util.timer";
|
||||
local ip_util = require "util.ip";
|
||||
local new_ip = ip_util.new_ip;
|
||||
local match_ip = ip_util.match;
|
||||
local parse_cidr = ip_util.parse_cidr;
|
||||
|
||||
local config = {};
|
||||
local limits_resolution = 1;
|
||||
|
||||
local function load_config()
|
||||
-- Max allowed login rate in events per second.
|
||||
config.login_rate = module:get_option_number("rate_limit_login_rate", 3);
|
||||
-- The rate to which sessions from IPs exceeding the join rate will be limited, in bytes per second.
|
||||
config.ip_rate = module:get_option_number("rate_limit_ip_rate", 2000);
|
||||
-- The rate to which sessions exceeding the stanza(iq, presence, message) rate will be limited, in bytes per second.
|
||||
config.session_rate = module:get_option_number("rate_limit_session_rate", 1000);
|
||||
-- The time in seconds, after which the limit for an IP address is lifted.
|
||||
config.timeout = module:get_option_number("rate_limit_timeout", 60);
|
||||
-- List of regular expressions for IP addresses that are not limited by this module.
|
||||
config.whitelist = module:get_option_set("rate_limit_whitelist", { "127.0.0.1", "::1" })._items;
|
||||
-- The size of the cache that saves state for IP addresses
|
||||
config.cache_size = module:get_option_number("rate_limit_cache_size", 10000);
|
||||
|
||||
-- Max allowed presence rate in events per second.
|
||||
config.presence_rate = module:get_option_number("rate_limit_presence_rate", 4);
|
||||
-- Max allowed iq rate in events per second.
|
||||
config.iq_rate = module:get_option_number("rate_limit_iq_rate", 15);
|
||||
-- Max allowed message rate in events per second.
|
||||
config.message_rate = module:get_option_number("rate_limit_message_rate", 3);
|
||||
|
||||
-- A list of hosts for which sessions we ignore rate limiting
|
||||
config.whitelist_hosts = module:get_option_set("rate_limit_whitelist_hosts", {});
|
||||
|
||||
local wl = "";
|
||||
for ip in config.whitelist do wl = wl .. ip .. "," end
|
||||
local wl_hosts = "";
|
||||
for j in config.whitelist_hosts do wl_hosts = wl_hosts .. j .. "," end
|
||||
module:log("info", "Loaded configuration: ");
|
||||
module:log("info", "- ip_rate=%s bytes/sec, session_rate=%s bytes/sec, timeout=%s sec, cache size=%s, whitelist=%s, whitelist_hosts=%s",
|
||||
config.ip_rate, config.session_rate, config.timeout, config.cache_size, wl, wl_hosts);
|
||||
module:log("info", "- login_rate=%s/sec, presence_rate=%s/sec, iq_rate=%s/sec, message_rate=%s/sec",
|
||||
config.login_rate, config.presence_rate, config.iq_rate, config.message_rate);
|
||||
end
|
||||
load_config();
|
||||
|
||||
-- Maps an IP address to a util.throttle which keeps the rate of login/join events from that IP.
|
||||
local login_rates = cache.new(config.cache_size);
|
||||
|
||||
-- Keeps the IP addresses that have exceeded the allowed login/join rate (i.e. the IP addresses whose sessions need
|
||||
-- to be limited). Mapped to the last instant at which the rate was exceeded.
|
||||
local limited_ips = cache.new(config.cache_size);
|
||||
|
||||
local function is_whitelisted(ip)
|
||||
local parsed_ip = new_ip(ip)
|
||||
for entry in config.whitelist do
|
||||
if match_ip(parsed_ip, parse_cidr(entry)) then
|
||||
return true;
|
||||
end
|
||||
end
|
||||
|
||||
return false;
|
||||
end
|
||||
|
||||
local function is_whitelisted_host(h)
|
||||
return config.whitelist_hosts:contains(h);
|
||||
end
|
||||
|
||||
-- Discover real remote IP of a session
|
||||
-- Note: http_server.get_request_from_conn() was added in Prosody 0.12.3,
|
||||
-- this code provides backwards compatibility with older versions
|
||||
local get_request_from_conn = http_server.get_request_from_conn or function (conn)
|
||||
local response = conn and conn._http_open_response;
|
||||
return response and response.request or nil;
|
||||
end;
|
||||
|
||||
-- Add an IP to the set of limied IPs
|
||||
local function limit_ip(ip)
|
||||
module:log("info", "Limiting %s due to login/join rate exceeded.", ip);
|
||||
limited_ips:set(ip, gettime());
|
||||
end
|
||||
|
||||
-- Installable as a session filter to limit the reading rate for a session. Based on mod_limits.
|
||||
local function limit_bytes_in(bytes, session)
|
||||
local sess_throttle = session.jitsi_throttle;
|
||||
if sess_throttle then
|
||||
-- if the limit timeout has elapsed let's stop the throttle
|
||||
if not sess_throttle.start or gettime() - sess_throttle.start > config.timeout then
|
||||
module:log("info", "Stop throttling session=%s, ip=%s.", session.id, session.ip);
|
||||
session.jitsi_throttle = nil;
|
||||
return bytes;
|
||||
end
|
||||
local ok, _, outstanding = sess_throttle:poll(#bytes, true);
|
||||
if not ok then
|
||||
session.log("debug",
|
||||
"Session over rate limit (%d) with %d (by %d), pausing", sess_throttle.max, #bytes, outstanding);
|
||||
outstanding = ceil(outstanding);
|
||||
session.conn:pause(); -- Read no more data from the connection until there is no outstanding data
|
||||
local outstanding_data = bytes:sub(-outstanding);
|
||||
bytes = bytes:sub(1, #bytes-outstanding);
|
||||
timer.add_task(limits_resolution, function ()
|
||||
if not session.conn then return; end
|
||||
if sess_throttle:peek(#outstanding_data) then
|
||||
session.log("debug", "Resuming paused session");
|
||||
session.conn:resume();
|
||||
end
|
||||
-- Handle what we can of the outstanding data
|
||||
session.data(outstanding_data);
|
||||
end);
|
||||
end
|
||||
end
|
||||
return bytes;
|
||||
end
|
||||
|
||||
-- Throttles reading from the connection of a specific session.
|
||||
local function throttle_session(session, rate, timeout)
|
||||
if not session.jitsi_throttle then
|
||||
if (session.conn and session.conn.setlimit) then
|
||||
session.jitsi_throttle_counter = session.jitsi_throttle_counter + 1;
|
||||
module:log("info", "Enabling throttle (%s bytes/s) via setlimit, session=%s, ip=%s, counter=%s.",
|
||||
rate, session.id, session.ip, session.jitsi_throttle_counter);
|
||||
session.conn:setlimit(rate);
|
||||
if timeout then
|
||||
if session.jitsi_throttle_timer then
|
||||
-- if there was a timer stop it as we will schedule a new one
|
||||
session.jitsi_throttle_timer:stop();
|
||||
session.jitsi_throttle_timer = nil;
|
||||
end
|
||||
session.jitsi_throttle_timer = module:add_timer(timeout, function()
|
||||
if session.conn then
|
||||
module:log("info", "Stop throttling session=%s, ip=%s.", session.id, session.ip);
|
||||
session.conn:setlimit(0);
|
||||
end
|
||||
session.jitsi_throttle_timer = nil;
|
||||
end);
|
||||
end
|
||||
else
|
||||
module:log("info", "Enabling throttle (%s bytes/s) via filter, session=%s, ip=%s.", rate, session.id, session.ip);
|
||||
session.jitsi_throttle = new_throttle(rate, 2);
|
||||
filters.add_filter(session, "bytes/in", limit_bytes_in, 1000);
|
||||
-- throttle.start used for stop throttling after the timeout
|
||||
session.jitsi_throttle.start = gettime();
|
||||
end
|
||||
else
|
||||
-- update the throttling start
|
||||
session.jitsi_throttle.start = gettime();
|
||||
end
|
||||
end
|
||||
|
||||
-- checks different stanzas for rate limiting (per session)
|
||||
function filter_stanza(stanza, session)
|
||||
local rate = session[stanza.name.."_rate"];
|
||||
if rate then
|
||||
local ok, _, _ = rate:poll(1, true);
|
||||
if not ok then
|
||||
module:log("info", "%s rate exceeded for %s, limiting.", stanza.name, session.full_jid);
|
||||
throttle_session(session, config.session_rate, config.timeout);
|
||||
end
|
||||
end
|
||||
|
||||
return stanza;
|
||||
end
|
||||
|
||||
local function on_login(session, ip)
|
||||
local login_rate = login_rates:get(ip);
|
||||
if not login_rate then
|
||||
module:log("debug", "Create new join rate for %s", ip);
|
||||
login_rate = new_throttle(config.login_rate, 2);
|
||||
login_rates:set(ip, login_rate);
|
||||
end
|
||||
|
||||
local ok, _, _ = login_rate:poll(1, true);
|
||||
if not ok then
|
||||
module:log("info", "Join rate exceeded for %s, limiting.", ip);
|
||||
limit_ip(ip);
|
||||
end
|
||||
end
|
||||
|
||||
local function filter_hook(session)
|
||||
-- ignore outgoing sessions (s2s)
|
||||
if session.outgoing then
|
||||
return;
|
||||
end
|
||||
|
||||
local request = get_request_from_conn(session.conn);
|
||||
local ip = request and request.ip or session.ip;
|
||||
module:log("debug", "New session from %s", ip);
|
||||
if is_whitelisted(ip) or is_whitelisted_host(session.host) then
|
||||
return;
|
||||
end
|
||||
|
||||
on_login(session, ip);
|
||||
|
||||
-- creates the stanzas rates
|
||||
session.jitsi_throttle_counter = 0;
|
||||
session.presence_rate = new_throttle(config.presence_rate, 2);
|
||||
session.iq_rate = new_throttle(config.iq_rate, 2);
|
||||
session.message_rate = new_throttle(config.message_rate, 2);
|
||||
filters.add_filter(session, "stanzas/in", filter_stanza);
|
||||
|
||||
local oldt = limited_ips:get(ip);
|
||||
if oldt then
|
||||
local newt = gettime();
|
||||
local elapsed = newt - oldt;
|
||||
if elapsed < config.timeout then
|
||||
if elapsed < 5 then
|
||||
module:log("info", "IP address %s was limited %s seconds ago, refreshing.", ip, elapsed);
|
||||
limited_ips:set(ip, newt);
|
||||
end
|
||||
throttle_session(session, config.ip_rate);
|
||||
else
|
||||
module:log("info", "Removing the limit for %s", ip);
|
||||
limited_ips:set(ip, nil);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function module.load()
|
||||
filters.add_filter_hook(filter_hook);
|
||||
end
|
||||
|
||||
function module.unload()
|
||||
filters.remove_filter_hook(filter_hook);
|
||||
end
|
||||
|
||||
module:hook_global("config-reloaded", load_config);
|
||||
|
||||
-- we calculate the stats on the configured interval (60 seconds by default)
|
||||
local measure_limited_ips = module:measure('limited-ips', 'amount'); -- we send stats for the total number limited ips
|
||||
module:hook_global('stats-update', function ()
|
||||
measure_limited_ips(limited_ips:count());
|
||||
end);
|
695
roles/jitsi/files/prosody/modules/mod_reservations.lua
Normal file
695
roles/jitsi/files/prosody/modules/mod_reservations.lua
Normal file
@ -0,0 +1,695 @@
|
||||
--- This is a port of Jicofo's Reservation System as a prosody module
|
||||
-- ref: https://github.com/jitsi/jicofo/blob/master/doc/reservation.md
|
||||
--
|
||||
-- We try to retain the same behaviour and interfaces where possible, but there
|
||||
-- is some difference:
|
||||
-- * In the event that the DELETE call fails, Jicofo's reservation
|
||||
-- system retains reservation data and allows re-creation of room if requested by
|
||||
-- the same creator without making further call to the API; this module does not
|
||||
-- offer this behaviour. Re-creation of a closed room will behave like a new meeting
|
||||
-- and trigger a new API call to validate the reservation.
|
||||
-- * Jicofo's reservation system expect int-based conflict_id. We take any sensible string.
|
||||
--
|
||||
-- In broad strokes, this module works by intercepting Conference IQs sent to focus component
|
||||
-- and buffers it until reservation is confirmed (by calling the provided API endpoint).
|
||||
-- The IQ events are routed on to focus component if reservation is valid, or error
|
||||
-- response is sent back to the origin if reservation is denied. Events are routed as usual
|
||||
-- if the room already exists.
|
||||
--
|
||||
--
|
||||
-- Installation:
|
||||
-- =============
|
||||
--
|
||||
-- Under domain config,
|
||||
-- 1. add "reservations" to modules_enabled.
|
||||
-- 2. Specify URL base for your API endpoint using "reservations_api_prefix" (required)
|
||||
-- 3. Optional config:
|
||||
-- * set "reservations_api_timeout" to change API call timeouts (defaults to 20 seconds)
|
||||
-- * set "reservations_api_headers" to specify custom HTTP headers included in
|
||||
-- all API calls e.g. to provide auth tokens.
|
||||
-- * set "reservations_api_retry_count" to the number of times API call failures are retried (defaults to 3)
|
||||
-- * set "reservations_api_retry_delay" seconds to wait between retries (defaults to 3s)
|
||||
-- * set "reservations_api_should_retry_for_code" to a function that takes an HTTP response code and
|
||||
-- returns true if API call should be retried. By default, retries are done for 5XX
|
||||
-- responses. Timeouts are never retried, and HTTP call failures are always retried.
|
||||
-- * set "reservations_enable_max_occupants" to true to enable integration with
|
||||
-- mod_muc_max_occupants. Setting thia will allow optional "max_occupants" (integer)
|
||||
-- payload from API to influence max occupants allowed for a given room.
|
||||
-- * set "reservations_enable_lobby_support" to true to enable integration
|
||||
-- with "muc_lobby_rooms". Setting this will allow optional "lobby" (boolean)
|
||||
-- fields in API payload. If set to true, Lobby will be enabled for the room.
|
||||
-- "persistent_lobby" module must also be enabled for this to work.
|
||||
-- * set "reservations_enable_password_support" to allow optional "password" (string)
|
||||
-- field in API payload. If set and not empty, then room password will be set
|
||||
-- to the given string.
|
||||
-- * By default, reservation checks are skipped for breakout rooms. You can subject
|
||||
-- breakout rooms to the same checks by setting "reservations_skip_breakout_rooms" to false.
|
||||
--
|
||||
--
|
||||
-- Example config:
|
||||
--
|
||||
-- VirtualHost "jitmeet.example.com"
|
||||
-- modules_enabled = {
|
||||
-- "reservations";
|
||||
-- }
|
||||
-- reservations_api_prefix = "http://reservation.example.com"
|
||||
--
|
||||
-- --- The following are all optional
|
||||
-- reservations_api_headers = {
|
||||
-- ["Authorization"] = "Bearer TOKEN-237958623045";
|
||||
-- }
|
||||
-- reservations_api_timeout = 10 -- timeout if API does not respond within 10s
|
||||
-- reservations_api_retry_count = 5 -- retry up to 5 times
|
||||
-- reservations_api_retry_delay = 1 -- wait 1s between retries
|
||||
-- reservations_api_should_retry_for_code = function (code)
|
||||
-- return code >= 500 or code == 408
|
||||
-- end
|
||||
--
|
||||
-- reservations_enable_max_occupants = true -- support "max_occupants" field
|
||||
-- reservations_enable_lobby_support = true -- support "lobby" field
|
||||
-- reservations_enable_password_support = true -- support "password" field
|
||||
--
|
||||
|
||||
local jid = require 'util.jid';
|
||||
local http = require "net.http";
|
||||
local json = require 'cjson.safe';
|
||||
local st = require "util.stanza";
|
||||
local timer = require 'util.timer';
|
||||
local datetime = require 'util.datetime';
|
||||
|
||||
local util = module:require "util";
|
||||
local get_room_from_jid = util.get_room_from_jid;
|
||||
local is_healthcheck_room = util.is_healthcheck_room;
|
||||
local room_jid_match_rewrite = util.room_jid_match_rewrite;
|
||||
local process_host_module = util.process_host_module;
|
||||
|
||||
local api_prefix = module:get_option("reservations_api_prefix");
|
||||
local api_headers = module:get_option("reservations_api_headers");
|
||||
local api_timeout = module:get_option("reservations_api_timeout", 20);
|
||||
local api_retry_count = tonumber(module:get_option("reservations_api_retry_count", 3));
|
||||
local api_retry_delay = tonumber(module:get_option("reservations_api_retry_delay", 3));
|
||||
local max_occupants_enabled = module:get_option("reservations_enable_max_occupants", false);
|
||||
local lobby_support_enabled = module:get_option("reservations_enable_lobby_support", false);
|
||||
local password_support_enabled = module:get_option("reservations_enable_password_support", false);
|
||||
local skip_breakout_room = module:get_option("reservations_skip_breakout_rooms", true);
|
||||
|
||||
|
||||
-- Option for user to control HTTP response codes that will result in a retry.
|
||||
-- Defaults to returning true on any 5XX code or 0
|
||||
local api_should_retry_for_code = module:get_option("reservations_api_should_retry_for_code", function (code)
|
||||
return code >= 500;
|
||||
end)
|
||||
|
||||
|
||||
local muc_component_host = module:get_option_string("main_muc");
|
||||
local breakout_muc_component_host = module:get_option_string('breakout_rooms_muc', 'breakout.'..module.host);
|
||||
|
||||
|
||||
-- How often to check and evict expired reservation data
|
||||
local expiry_check_period = 60;
|
||||
|
||||
|
||||
-- Cannot proceed if "reservations_api_prefix" not configured
|
||||
if not api_prefix then
|
||||
module:log("error", "reservations_api_prefix not specified. Disabling %s", module:get_name());
|
||||
return;
|
||||
end
|
||||
|
||||
|
||||
-- get/infer focus component hostname so we can intercept IQ bound for it
|
||||
local focus_component_host = module:get_option_string("focus_component");
|
||||
if not focus_component_host then
|
||||
local muc_domain_base = module:get_option_string("muc_mapper_domain_base");
|
||||
if not muc_domain_base then
|
||||
module:log("error", "Could not infer focus domain. Disabling %s", module:get_name());
|
||||
return;
|
||||
end
|
||||
focus_component_host = 'focus.'..muc_domain_base;
|
||||
end
|
||||
|
||||
-- common HTTP headers added to all API calls
|
||||
local http_headers = {
|
||||
["User-Agent"] = "Prosody ("..prosody.version.."; "..prosody.platform..")";
|
||||
};
|
||||
if api_headers then -- extra headers from config
|
||||
for key, value in pairs(api_headers) do
|
||||
http_headers[key] = value;
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
--- Utils
|
||||
|
||||
--- Converts int timestamp to datetime string compatible with Java SimpleDateFormat
|
||||
-- @param t timestamps in seconds. Supports int (as returned by os.time()) or higher
|
||||
-- precision (as returned by socket.gettime())
|
||||
-- @return formatted datetime string (yyyy-MM-dd'T'HH:mm:ss.SSSX)
|
||||
local function to_java_date_string(t)
|
||||
local t_secs, mantissa = math.modf(t);
|
||||
local ms_str = (mantissa == 0) and '.000' or tostring(mantissa):sub(2,5);
|
||||
local date_str = os.date("!%Y-%m-%dT%H:%M:%S", t_secs);
|
||||
return date_str..ms_str..'Z';
|
||||
end
|
||||
|
||||
|
||||
--- Start non-blocking HTTP call
|
||||
-- @param url URL to call
|
||||
-- @param options options table as expected by net.http where we provide optional headers, body or method.
|
||||
-- @param callback if provided, called with callback(response_body, response_code) when call complete.
|
||||
-- @param timeout_callback if provided, called without args when request times out.
|
||||
-- @param retries how many times to retry on failure; 0 means no retries.
|
||||
local function async_http_request(url, options, callback, timeout_callback, retries)
|
||||
local completed = false;
|
||||
local timed_out = false;
|
||||
local retries = retries or api_retry_count;
|
||||
|
||||
local function cb_(response_body, response_code)
|
||||
if not timed_out then -- request completed before timeout
|
||||
completed = true;
|
||||
if (response_code == 0 or api_should_retry_for_code(response_code)) and retries > 0 then
|
||||
module:log("warn", "API Response code %d. Will retry after %ds", response_code, api_retry_delay);
|
||||
timer.add_task(api_retry_delay, function()
|
||||
async_http_request(url, options, callback, timeout_callback, retries - 1)
|
||||
end)
|
||||
return;
|
||||
end
|
||||
|
||||
if callback then
|
||||
callback(response_body, response_code)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local request = http.request(url, options, cb_);
|
||||
|
||||
timer.add_task(api_timeout, function ()
|
||||
timed_out = true;
|
||||
|
||||
if not completed then
|
||||
http.destroy_request(request);
|
||||
if timeout_callback then
|
||||
timeout_callback()
|
||||
end
|
||||
end
|
||||
end);
|
||||
|
||||
end
|
||||
|
||||
--- Returns current timestamp
|
||||
local function now()
|
||||
-- Don't really need higher precision of socket.gettime(). Besides, we loose
|
||||
-- milliseconds precision when converting back to timestamp from date string
|
||||
-- when we use datetime.parse(t), so let's be consistent.
|
||||
return os.time();
|
||||
end
|
||||
|
||||
--- Start RoomReservation implementation
|
||||
|
||||
-- Status enums used in RoomReservation:meta.status
|
||||
local STATUS = {
|
||||
PENDING = 0;
|
||||
SUCCESS = 1;
|
||||
FAILED = -1;
|
||||
}
|
||||
|
||||
local RoomReservation = {};
|
||||
RoomReservation.__index = RoomReservation;
|
||||
|
||||
function newRoomReservation(room_jid, creator_jid)
|
||||
return setmetatable({
|
||||
room_jid = room_jid;
|
||||
|
||||
-- Reservation metadata. store as table so we can set and read atomically.
|
||||
-- N.B. This should always be updated using self.set_status_*
|
||||
meta = {
|
||||
status = STATUS.PENDING;
|
||||
mail_owner = jid.bare(creator_jid);
|
||||
conflict_id = nil;
|
||||
start_time = now(); -- timestamp, in seconds
|
||||
expires_at = nil; -- timestamp, in seconds
|
||||
error_text = nil;
|
||||
error_code = nil;
|
||||
};
|
||||
|
||||
-- Array of pending events that we need to route once API call is complete
|
||||
pending_events = {};
|
||||
|
||||
-- Set true when API call trigger has been triggered (by enqueue of first event)
|
||||
api_call_triggered = false;
|
||||
}, RoomReservation);
|
||||
end
|
||||
|
||||
|
||||
--- Extracts room name from room jid
|
||||
function RoomReservation:get_room_name()
|
||||
return jid.node(self.room_jid);
|
||||
end
|
||||
|
||||
--- Checks if reservation data is expires and should be evicted from store
|
||||
function RoomReservation:is_expired()
|
||||
return self.meta.expires_at ~= nil and now() > self.meta.expires_at;
|
||||
end
|
||||
|
||||
--- Main entry point for handing and routing events.
|
||||
function RoomReservation:enqueue_or_route_event(event)
|
||||
if self.meta.status == STATUS.PENDING then
|
||||
table.insert(self.pending_events, event)
|
||||
if self.api_call_triggered ~= true then
|
||||
self:call_api_create_conference();
|
||||
end
|
||||
else
|
||||
-- API call already complete. Immediately route without enqueueing.
|
||||
-- This could happen if request comes in between the time reservation approved
|
||||
-- and when Jicofo actually creates the room.
|
||||
module:log("debug", "Reservation details already stored. Skipping queue for %s", self.room_jid);
|
||||
self:route_event(event);
|
||||
end
|
||||
end
|
||||
|
||||
--- Updates status and initiates event routing. Called internally when API call complete.
|
||||
function RoomReservation:set_status_success(start_time, duration, mail_owner, conflict_id, data)
|
||||
module:log("info", "Reservation created successfully for %s", self.room_jid);
|
||||
self.meta = {
|
||||
status = STATUS.SUCCESS;
|
||||
mail_owner = mail_owner or self.meta.mail_owner;
|
||||
conflict_id = conflict_id;
|
||||
start_time = start_time;
|
||||
expires_at = start_time + duration;
|
||||
error_text = nil;
|
||||
error_code = nil;
|
||||
}
|
||||
if max_occupants_enabled and data.max_occupants then
|
||||
self.meta.max_occupants = data.max_occupants
|
||||
end
|
||||
if lobby_support_enabled and data.lobby then
|
||||
self.meta.lobby = data.lobby
|
||||
end
|
||||
if password_support_enabled and data.password then
|
||||
self.meta.password = data.password
|
||||
end
|
||||
self:route_pending_events()
|
||||
end
|
||||
|
||||
--- Updates status and initiates error response to pending events. Called internally when API call complete.
|
||||
function RoomReservation:set_status_failed(error_code, error_text)
|
||||
module:log("info", "Reservation creation failed for %s - (%s) %s", self.room_jid, error_code, error_text);
|
||||
self.meta = {
|
||||
status = STATUS.FAILED;
|
||||
mail_owner = self.meta.mail_owner;
|
||||
conflict_id = nil;
|
||||
start_time = self.meta.start_time;
|
||||
-- Retain reservation rejection for a short while so we have time to report failure to
|
||||
-- existing clients and not trigger a re-query too soon.
|
||||
-- N.B. Expiry could take longer since eviction happens periodically.
|
||||
expires_at = now() + 30;
|
||||
error_text = error_text;
|
||||
error_code = error_code;
|
||||
}
|
||||
self:route_pending_events()
|
||||
end
|
||||
|
||||
--- Triggers routing of all enqueued events
|
||||
function RoomReservation:route_pending_events()
|
||||
if self.meta.status == STATUS.PENDING then -- should never be called while PENDING. check just in case.
|
||||
return;
|
||||
end
|
||||
|
||||
module:log("debug", "Routing all pending events for %s", self.room_jid);
|
||||
local event;
|
||||
|
||||
while #self.pending_events ~= 0 do
|
||||
event = table.remove(self.pending_events);
|
||||
self:route_event(event)
|
||||
end
|
||||
end
|
||||
|
||||
--- Event routing implementation
|
||||
function RoomReservation:route_event(event)
|
||||
-- this should only be called after API call complete and status no longer PENDING
|
||||
assert(self.meta.status ~= STATUS.PENDING, "Attempting to route event while API call still PENDING")
|
||||
|
||||
local meta = self.meta;
|
||||
local origin, stanza = event.origin, event.stanza;
|
||||
|
||||
if meta.status == STATUS.FAILED then
|
||||
module:log("debug", "Route: Sending reservation error to %s", stanza.attr.from);
|
||||
self:reply_with_error(event, meta.error_code, meta.error_text);
|
||||
else
|
||||
if meta.status == STATUS.SUCCESS then
|
||||
if self:is_expired() then
|
||||
module:log("debug", "Route: Sending reservation expiry to %s", stanza.attr.from);
|
||||
self:reply_with_error(event, 419, "Reservation expired");
|
||||
else
|
||||
module:log("debug", "Route: Forwarding on event from %s", stanza.attr.from);
|
||||
prosody.core_post_stanza(origin, stanza, false); -- route iq to intended target (focus)
|
||||
end
|
||||
else
|
||||
-- this should never happen unless dev made a mistake. Block by default just in case.
|
||||
module:log("error", "Reservation for %s has invalid state %s. Rejecting request.", self.room_jid, meta.status);
|
||||
self:reply_with_error(event, 500, "Failed to determine reservation state");
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Generates reservation-error stanza and sends to event origin.
|
||||
function RoomReservation:reply_with_error(event, error_code, error_text)
|
||||
local stanza = event.stanza;
|
||||
local id = stanza.attr.id;
|
||||
local to = stanza.attr.from;
|
||||
local from = stanza.attr.to;
|
||||
|
||||
event.origin.send(
|
||||
st.iq({ type="error", to=to, from=from, id=id })
|
||||
:tag("error", { type="cancel" })
|
||||
:tag("service-unavailable", { xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" }):up()
|
||||
:tag("text", { xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" }):text(error_text):up()
|
||||
:tag("reservation-error", { xmlns="http://jitsi.org/protocol/focus", ["error-code"]=tostring(error_code) })
|
||||
);
|
||||
end
|
||||
|
||||
--- Initiates non-blocking API call to validate reservation
|
||||
function RoomReservation:call_api_create_conference()
|
||||
self.api_call_triggered = true;
|
||||
|
||||
local url = api_prefix..'/conference';
|
||||
local request_data = {
|
||||
name = self:get_room_name();
|
||||
start_time = to_java_date_string(self.meta.start_time);
|
||||
mail_owner = self.meta.mail_owner;
|
||||
}
|
||||
|
||||
local http_options = {
|
||||
body = http.formencode(request_data); -- because Jicofo reservation encodes as form data instead JSON
|
||||
method = 'POST';
|
||||
headers = http_headers;
|
||||
}
|
||||
|
||||
module:log("debug", "Sending POST /conference for %s", self.room_jid);
|
||||
async_http_request(url, http_options, function (response_body, response_code)
|
||||
self:on_api_create_conference_complete(response_body, response_code);
|
||||
end, function ()
|
||||
self:on_api_call_timeout();
|
||||
end);
|
||||
end
|
||||
|
||||
--- Parses and validates HTTP response body for conference payload
|
||||
-- Ref: https://github.com/jitsi/jicofo/blob/master/doc/reservation.md
|
||||
-- @return nil if invalid, or table with payload parsed from JSON response
|
||||
function RoomReservation:parse_conference_response(response_body)
|
||||
local data, error = json.decode(response_body);
|
||||
|
||||
if data == nil then -- invalid JSON payload
|
||||
module:log("error", "Invalid JSON response from API - %s error:%s", response_body, error);
|
||||
return;
|
||||
end
|
||||
|
||||
if data.name == nil or data.name:lower() ~= self:get_room_name() then
|
||||
module:log("error", "Missing or mismatching room name - %s", data.name);
|
||||
return;
|
||||
end
|
||||
|
||||
if data.id == nil then
|
||||
module:log("error", "Missing id");
|
||||
return;
|
||||
end
|
||||
|
||||
if data.mail_owner == nil then
|
||||
module:log("error", "Missing mail_owner");
|
||||
return;
|
||||
end
|
||||
|
||||
local duration = tonumber(data.duration);
|
||||
if duration == nil then
|
||||
module:log("error", "Missing or invalid duration - %s", data.duration);
|
||||
return;
|
||||
end
|
||||
data.duration = duration;
|
||||
|
||||
-- if optional "max_occupants" field set, cast to number
|
||||
if data.max_occupants ~= nil then
|
||||
local max_occupants = tonumber(data.max_occupants)
|
||||
if max_occupants == nil or max_occupants < 1 then
|
||||
-- N.B. invalid max_occupants rejected even if max_occupants_enabled=false
|
||||
module:log("error", "Invalid value for max_occupants - %s", data.max_occupants);
|
||||
return;
|
||||
end
|
||||
data.max_occupants = max_occupants
|
||||
end
|
||||
|
||||
-- if optional "lobby" field set, accept boolean true or "true"
|
||||
if data.lobby ~= nil then
|
||||
if (type(data.lobby) == "boolean" and data.lobby) or data.lobby == "true" then
|
||||
data.lobby = true
|
||||
else
|
||||
data.lobby = false
|
||||
end
|
||||
end
|
||||
|
||||
-- if optional "password" field set, it has to be string
|
||||
if data.password ~= nil then
|
||||
if type(data.password) ~= "string" then
|
||||
-- N.B. invalid "password" rejected even if reservations_enable_password_support=false
|
||||
module:log("error", "Invalid type for password - string expected");
|
||||
return;
|
||||
end
|
||||
end
|
||||
|
||||
local start_time = datetime.parse(data.start_time); -- N.B. we lose milliseconds portion of the date
|
||||
if start_time == nil then
|
||||
module:log("error", "Missing or invalid start_time - %s", data.start_time);
|
||||
return;
|
||||
end
|
||||
data.start_time = start_time;
|
||||
|
||||
return data;
|
||||
end
|
||||
|
||||
--- Parses and validates HTTP error response body for API call.
|
||||
-- Expect JSON with a "message" field.
|
||||
-- @return message string, or generic error message if invalid payload.
|
||||
function RoomReservation:parse_error_message_from_response(response_body)
|
||||
local data = json.decode(response_body);
|
||||
if data ~= nil and data.message ~= nil then
|
||||
module:log("debug", "Invalid error response body. Will use generic error message.");
|
||||
return data.message;
|
||||
else
|
||||
return "Rejected by reservation server";
|
||||
end
|
||||
end
|
||||
|
||||
--- callback on API timeout
|
||||
function RoomReservation:on_api_call_timeout()
|
||||
self:set_status_failed(500, 'Reservation lookup timed out');
|
||||
end
|
||||
|
||||
--- callback on API response
|
||||
function RoomReservation:on_api_create_conference_complete(response_body, response_code)
|
||||
if response_code == 200 or response_code == 201 then
|
||||
self:handler_conference_data_returned_from_api(response_body);
|
||||
elseif response_code == 409 then
|
||||
self:handle_conference_already_exist(response_body);
|
||||
elseif response_code == nil then -- warrants a retry, but this should be done automatically by the http call method.
|
||||
self:set_status_failed(500, 'Could not contact reservation server');
|
||||
else
|
||||
self:set_status_failed(response_code, self:parse_error_message_from_response(response_body));
|
||||
end
|
||||
end
|
||||
|
||||
function RoomReservation:handler_conference_data_returned_from_api(response_body)
|
||||
local data = self:parse_conference_response(response_body);
|
||||
if not data then -- invalid response from API
|
||||
module:log("error", "API returned success code but invalid payload");
|
||||
self:set_status_failed(500, 'Invalid response from reservation server');
|
||||
else
|
||||
self:set_status_success(data.start_time, data.duration, data.mail_owner, data.id, data)
|
||||
end
|
||||
end
|
||||
|
||||
function RoomReservation:handle_conference_already_exist(response_body)
|
||||
local data = json.decode(response_body);
|
||||
if data == nil or data.conflict_id == nil then
|
||||
-- yes, in the case of 409, API expected to return "id" as "conflict_id".
|
||||
self:set_status_failed(409, 'Invalid response from reservation server');
|
||||
else
|
||||
local url = api_prefix..'/conference/'..data.conflict_id;
|
||||
local http_options = {
|
||||
method = 'GET';
|
||||
headers = http_headers;
|
||||
}
|
||||
|
||||
async_http_request(url, http_options, function(response_body, response_code)
|
||||
if response_code == 200 then
|
||||
self:handler_conference_data_returned_from_api(response_body);
|
||||
else
|
||||
self:set_status_failed(response_code, self:parse_error_message_from_response(response_body));
|
||||
end
|
||||
end, function ()
|
||||
self:on_api_call_timeout();
|
||||
end);
|
||||
end
|
||||
end
|
||||
|
||||
--- End RoomReservation
|
||||
|
||||
--- Store reservations lookups that are still pending or with room still active
|
||||
local reservations = {}
|
||||
|
||||
local function get_or_create_reservations(room_jid, creator_jid)
|
||||
if reservations[room_jid] == nil then
|
||||
module:log("debug", "Creating new reservation data for %s", room_jid);
|
||||
reservations[room_jid] = newRoomReservation(room_jid, creator_jid);
|
||||
end
|
||||
|
||||
return reservations[room_jid];
|
||||
end
|
||||
|
||||
local function evict_expired_reservations()
|
||||
local expired = {}
|
||||
|
||||
-- first, gather jids of expired rooms. So we don't remove from table while iterating.
|
||||
for room_jid, res in pairs(reservations) do
|
||||
if res:is_expired() then
|
||||
table.insert(expired, room_jid);
|
||||
end
|
||||
end
|
||||
|
||||
local room;
|
||||
for _, room_jid in ipairs(expired) do
|
||||
room = get_room_from_jid(room_jid);
|
||||
if room then
|
||||
-- Close room if still active (reservation duration exceeded)
|
||||
module:log("info", "Room exceeded reservation duration. Terminating %s", room_jid);
|
||||
room:destroy(nil, "Scheduled conference duration exceeded.");
|
||||
-- Rely on room_destroyed to calls DELETE /conference and drops reservation[room_jid]
|
||||
else
|
||||
module:log("error", "Reservation references expired room that is no longer active. Dropping %s", room_jid);
|
||||
-- This should not happen unless evict_expired_reservations somehow gets triggered
|
||||
-- between the time room is destroyed and room_destroyed callback is called. (Possible?)
|
||||
-- But just in case, we drop the reservation to avoid repeating this path on every pass.
|
||||
reservations[room_jid] = nil;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
timer.add_task(expiry_check_period, function()
|
||||
evict_expired_reservations();
|
||||
return expiry_check_period;
|
||||
end)
|
||||
|
||||
|
||||
--- Intercept conference IQ to Jicofo handle reservation checks before allowing normal event flow
|
||||
module:log("info", "Hook to global pre-iq/host");
|
||||
module:hook("pre-iq/host", function(event)
|
||||
local stanza = event.stanza;
|
||||
|
||||
if stanza.name ~= "iq" or stanza.attr.to ~= focus_component_host or stanza.attr.type ~= 'set' then
|
||||
return; -- not IQ for jicofo. Ignore this event.
|
||||
end
|
||||
|
||||
local conference = stanza:get_child('conference', 'http://jitsi.org/protocol/focus');
|
||||
if conference == nil then
|
||||
return; -- not Conference IQ. Ignore.
|
||||
end
|
||||
|
||||
local room_jid = room_jid_match_rewrite(conference.attr.room);
|
||||
|
||||
if get_room_from_jid(room_jid) ~= nil then
|
||||
module:log("debug", "Skip reservation check for existing room %s", room_jid);
|
||||
return; -- room already exists. Continue with normal flow
|
||||
end
|
||||
|
||||
if skip_breakout_room then
|
||||
local _, host = jid.split(room_jid);
|
||||
if host == breakout_muc_component_host then
|
||||
module:log("debug", "Skip reservation check for breakout room %s", room_jid);
|
||||
return;
|
||||
end
|
||||
end
|
||||
|
||||
local res = get_or_create_reservations(room_jid, stanza.attr.from);
|
||||
res:enqueue_or_route_event(event); -- hand over to reservation obj to route event
|
||||
return true;
|
||||
|
||||
end);
|
||||
|
||||
|
||||
--- Forget reservation details once room destroyed so query is repeated if room re-created
|
||||
local function room_destroyed(event)
|
||||
local res;
|
||||
local room = event.room
|
||||
|
||||
if not is_healthcheck_room(room.jid) then
|
||||
res = reservations[room.jid]
|
||||
|
||||
-- drop reservation data for this room
|
||||
reservations[room.jid] = nil
|
||||
|
||||
if res then -- just in case event triggered more than once?
|
||||
module:log("info", "Dropped reservation data for destroyed room %s", room.jid);
|
||||
|
||||
local conflict_id = res.meta.conflict_id
|
||||
if conflict_id then
|
||||
local url = api_prefix..'/conference/'..conflict_id;
|
||||
local http_options = {
|
||||
method = 'DELETE';
|
||||
headers = http_headers;
|
||||
}
|
||||
|
||||
module:log("debug", "Sending DELETE /conference/%s", conflict_id);
|
||||
async_http_request(url, http_options);
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local function room_created(event)
|
||||
local room = event.room
|
||||
|
||||
if is_healthcheck_room(room.jid) then
|
||||
return;
|
||||
end
|
||||
|
||||
local res = reservations[room.jid]
|
||||
|
||||
if res and max_occupants_enabled and res.meta.max_occupants ~= nil then
|
||||
module:log("info", "Setting max_occupants %d for room %s", res.meta.max_occupants, room.jid);
|
||||
room._data.max_occupants = res.meta.max_occupants
|
||||
end
|
||||
|
||||
if res and password_support_enabled and res.meta.password ~= nil then
|
||||
module:log("info", "Setting password for room %s", room.jid);
|
||||
room:set_password(res.meta.password);
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local function room_pre_create(event)
|
||||
local room = event.room
|
||||
|
||||
if is_healthcheck_room(room.jid) then
|
||||
return;
|
||||
end
|
||||
|
||||
local res = reservations[room.jid]
|
||||
|
||||
if res and lobby_support_enabled and res.meta.lobby then
|
||||
module:log("info", "Enabling lobby for room %s", room.jid);
|
||||
prosody.events.fire_event("create-persistent-lobby-room", { room = room; });
|
||||
end
|
||||
end
|
||||
|
||||
process_host_module(muc_component_host, function(host_module, host)
|
||||
module:log("info", "Hook to muc-room-destroyed on %s", host);
|
||||
host_module:hook("muc-room-destroyed", room_destroyed, -1);
|
||||
|
||||
if max_occupants_enabled or password_support_enabled then
|
||||
module:log("info", "Hook to muc-room-created on %s (max_occupants or password integration enabled)", host);
|
||||
host_module:hook("muc-room-created", room_created);
|
||||
end
|
||||
|
||||
if lobby_support_enabled then
|
||||
module:log("info", "Hook to muc-room-pre-create on %s (lobby integration enabled)", host);
|
||||
host_module:hook("muc-room-pre-create", room_pre_create);
|
||||
end
|
||||
end);
|
15
roles/jitsi/files/prosody/modules/mod_room_destroy.lua
Normal file
15
roles/jitsi/files/prosody/modules/mod_room_destroy.lua
Normal file
@ -0,0 +1,15 @@
|
||||
-- Handle room destroy requests it such a way that it can be suppressed by other
|
||||
-- modules that handle room lifecycle and wish to keep the room alive.
|
||||
|
||||
function handle_room_destroy(event)
|
||||
local room = event.room;
|
||||
local reason = event.reason;
|
||||
local caller = event.caller;
|
||||
|
||||
module:log('info', 'Destroying room %s (requested by %s)', room.jid, caller);
|
||||
room:set_persistent(false);
|
||||
room:destroy(nil, reason);
|
||||
end
|
||||
|
||||
module:hook_global("maybe-destroy-room", handle_room_destroy, -1);
|
||||
module:log('info', 'loaded');
|
10
roles/jitsi/files/prosody/modules/mod_room_metadata.lua
Normal file
10
roles/jitsi/files/prosody/modules/mod_room_metadata.lua
Normal file
@ -0,0 +1,10 @@
|
||||
-- Generic room metadata
|
||||
-- See mod_room_metadata_component.lua
|
||||
|
||||
local COMPONENT_IDENTITY_TYPE = 'room_metadata';
|
||||
local room_metadata_component_host = module:get_option_string('room_metadata_component', 'metadata.'..module.host);
|
||||
|
||||
module:depends("jitsi_session");
|
||||
|
||||
-- Advertise the component so clients can pick up the address and use it
|
||||
module:add_identity('component', COMPONENT_IDENTITY_TYPE, room_metadata_component_host);
|
@ -0,0 +1,240 @@
|
||||
-- This module implements a generic metadata storage system for rooms.
|
||||
--
|
||||
-- VirtualHost "jitmeet.example.com"
|
||||
-- modules_enabled = {
|
||||
-- "room_metadata"
|
||||
-- }
|
||||
-- room_metadata_component = "metadata.jitmeet.example.com"
|
||||
-- main_muc = "conference.jitmeet.example.com"
|
||||
--
|
||||
-- Component "metadata.jitmeet.example.com" "room_metadata_component"
|
||||
-- muc_component = "conference.jitmeet.example.com"
|
||||
-- breakout_rooms_component = "breakout.jitmeet.example.com"
|
||||
|
||||
local jid_node = require 'util.jid'.node;
|
||||
local json = require 'cjson.safe';
|
||||
local st = require 'util.stanza';
|
||||
|
||||
local util = module:require 'util';
|
||||
local is_healthcheck_room = util.is_healthcheck_room;
|
||||
local get_room_from_jid = util.get_room_from_jid;
|
||||
local room_jid_match_rewrite = util.room_jid_match_rewrite;
|
||||
local internal_room_jid_match_rewrite = util.internal_room_jid_match_rewrite;
|
||||
local process_host_module = util.process_host_module;
|
||||
|
||||
local COMPONENT_IDENTITY_TYPE = 'room_metadata';
|
||||
local FORM_KEY = 'muc#roominfo_jitsimetadata';
|
||||
|
||||
local muc_component_host = module:get_option_string('muc_component');
|
||||
|
||||
if muc_component_host == nil then
|
||||
module:log("error", "No muc_component specified. No muc to operate on!");
|
||||
return;
|
||||
end
|
||||
|
||||
local breakout_rooms_component_host = module:get_option_string('breakout_rooms_component');
|
||||
|
||||
module:log("info", "Starting room metadata for %s", muc_component_host);
|
||||
|
||||
local main_muc_module;
|
||||
|
||||
-- Utility functions
|
||||
|
||||
function getMetadataJSON(room)
|
||||
local res, error = json.encode({
|
||||
type = COMPONENT_IDENTITY_TYPE,
|
||||
metadata = room.jitsiMetadata or {}
|
||||
});
|
||||
|
||||
if not res then
|
||||
module:log('error', 'Error encoding data room:%s', room.jid, error);
|
||||
end
|
||||
|
||||
return res;
|
||||
end
|
||||
|
||||
-- Putting the information on the config form / disco-info allows us to save
|
||||
-- an extra message to users who join later.
|
||||
function getFormData(room)
|
||||
return {
|
||||
name = FORM_KEY;
|
||||
type = 'text-multi';
|
||||
label = 'Room metadata';
|
||||
value = getMetadataJSON(room);
|
||||
};
|
||||
end
|
||||
|
||||
function broadcastMetadata(room)
|
||||
local json_msg = getMetadataJSON(room);
|
||||
|
||||
for _, occupant in room:each_occupant() do
|
||||
send_json_msg(occupant.jid, internal_room_jid_match_rewrite(room.jid), json_msg)
|
||||
end
|
||||
end
|
||||
|
||||
function send_json_msg(to_jid, room_jid, json_msg)
|
||||
local stanza = st.message({ from = module.host; to = to_jid; })
|
||||
:tag('json-message', { xmlns = 'http://jitsi.org/jitmeet', room = room_jid }):text(json_msg):up();
|
||||
module:send(stanza);
|
||||
end
|
||||
|
||||
-- Handling events
|
||||
|
||||
function room_created(event)
|
||||
local room = event.room;
|
||||
|
||||
if is_healthcheck_room(room.jid) then
|
||||
return ;
|
||||
end
|
||||
|
||||
if not room.jitsiMetadata then
|
||||
room.jitsiMetadata = {};
|
||||
end
|
||||
end
|
||||
|
||||
function on_message(event)
|
||||
local session = event.origin;
|
||||
|
||||
-- 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 not session or not session.jitsi_web_query_room then
|
||||
return false;
|
||||
end
|
||||
|
||||
local message = event.stanza:get_child(COMPONENT_IDENTITY_TYPE, 'http://jitsi.org/jitmeet');
|
||||
local messageText = message:get_text();
|
||||
|
||||
if not message or not messageText then
|
||||
return false;
|
||||
end
|
||||
|
||||
local roomJid = message.attr.room;
|
||||
local room = get_room_from_jid(room_jid_match_rewrite(roomJid));
|
||||
|
||||
if not room then
|
||||
module:log('warn', 'No room found found for %s/%s',
|
||||
session.jitsi_web_query_prefix, session.jitsi_web_query_room);
|
||||
return false;
|
||||
end
|
||||
|
||||
-- check that the participant requesting is a moderator and is an occupant in the room
|
||||
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, room.jid);
|
||||
return false;
|
||||
end
|
||||
|
||||
if occupant.role ~= 'moderator' then
|
||||
module:log('warn', 'Occupant %s is not moderator and not allowed this operation for %s', from, room.jid);
|
||||
return false;
|
||||
end
|
||||
|
||||
local jsonData, error = json.decode(messageText);
|
||||
if jsonData == nil then -- invalid JSON
|
||||
module:log("error", "Invalid JSON message: %s error:%s", messageText, error);
|
||||
return false;
|
||||
end
|
||||
|
||||
if jsonData.key == nil or jsonData.data == nil then
|
||||
module:log("error", "Invalid JSON payload, key or data are missing: %s", messageText);
|
||||
return false;
|
||||
end
|
||||
|
||||
room.jitsiMetadata[jsonData.key] = jsonData.data;
|
||||
|
||||
broadcastMetadata(room);
|
||||
|
||||
-- fire and event for the change
|
||||
main_muc_module:fire_event('jitsi-metadata-updated', { room = room; actor = occupant; key = jsonData.key; });
|
||||
|
||||
return true;
|
||||
end
|
||||
|
||||
-- Module operations
|
||||
|
||||
-- handle messages to this component
|
||||
module:hook("message/host", on_message);
|
||||
|
||||
-- operates on already loaded main muc module
|
||||
function process_main_muc_loaded(main_muc, host_module)
|
||||
main_muc_module = host_module;
|
||||
|
||||
module:log('debug', 'Main muc loaded');
|
||||
module:log("info", "Hook to muc events on %s", muc_component_host);
|
||||
|
||||
host_module:hook("muc-room-created", room_created, -1);
|
||||
|
||||
host_module:hook('muc-disco#info', function (event)
|
||||
local room = event.room;
|
||||
|
||||
table.insert(event.form, getFormData(room));
|
||||
end);
|
||||
|
||||
host_module:hook("muc-config-form", function(event)
|
||||
local room = event.room;
|
||||
|
||||
table.insert(event.form, getFormData(room));
|
||||
end);
|
||||
-- The room metadata was updated internally (from another module).
|
||||
host_module:hook("room-metadata-changed", function(event)
|
||||
broadcastMetadata(event.room);
|
||||
end);
|
||||
end
|
||||
|
||||
-- process or waits to process the main muc component
|
||||
process_host_module(muc_component_host, function(host_module, host)
|
||||
local muc_module = prosody.hosts[host].modules.muc;
|
||||
|
||||
if muc_module then
|
||||
process_main_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_main_muc_loaded(prosody.hosts[host].modules.muc, host_module);
|
||||
end
|
||||
end);
|
||||
end
|
||||
end);
|
||||
|
||||
-- breakout rooms support
|
||||
function process_breakout_muc_loaded(breakout_muc, host_module)
|
||||
module:log('debug', 'Breakout rooms muc loaded');
|
||||
module:log("info", "Hook to muc events on %s", breakout_rooms_component_host);
|
||||
|
||||
host_module:hook("muc-room-created", room_created, -1);
|
||||
|
||||
host_module:hook('muc-disco#info', function (event)
|
||||
local room = event.room;
|
||||
|
||||
table.insert(event.form, getFormData(room));
|
||||
end);
|
||||
|
||||
host_module:hook("muc-config-form", function(event)
|
||||
local room = event.room;
|
||||
|
||||
table.insert(event.form, getFormData(room));
|
||||
end);
|
||||
end
|
||||
|
||||
if breakout_rooms_component_host then
|
||||
process_host_module(breakout_rooms_component_host, function(host_module, host)
|
||||
local muc_module = prosody.hosts[host].modules.muc;
|
||||
|
||||
if muc_module then
|
||||
process_breakout_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_breakout_muc_loaded(prosody.hosts[host].modules.muc, host_module);
|
||||
end
|
||||
end);
|
||||
end
|
||||
end);
|
||||
end
|
164
roles/jitsi/files/prosody/modules/mod_roster_command.lua
Normal file
164
roles/jitsi/files/prosody/modules/mod_roster_command.lua
Normal file
@ -0,0 +1,164 @@
|
||||
-----------------------------------------------------------
|
||||
-- mod_roster_command: Manage rosters through prosodyctl
|
||||
-- version 0.02
|
||||
-----------------------------------------------------------
|
||||
-- Copyright (C) 2011 Matthew Wild
|
||||
-- Copyright (C) 2011 Adam Nielsen
|
||||
--
|
||||
-- This project is MIT/X11 licensed. Please see the
|
||||
-- COPYING file in the source package for more information.
|
||||
-----------------------------------------------------------
|
||||
|
||||
if module.host ~= "*" then
|
||||
module:log("error", "Do not load this module in Prosody, for correct usage see: https://modules.prosody.im/mod_roster_command.html");
|
||||
return;
|
||||
end
|
||||
|
||||
|
||||
-- Workaround for lack of util.startup...
|
||||
local prosody = _G.prosody;
|
||||
local hosts = prosody.hosts;
|
||||
prosody.bare_sessions = prosody.bare_sessions or {};
|
||||
_G.bare_sessions = _G.bare_sessions or prosody.bare_sessions;
|
||||
|
||||
local usermanager = require "core.usermanager";
|
||||
local rostermanager = require "core.rostermanager";
|
||||
local storagemanager = require "core.storagemanager";
|
||||
local jid = require "util.jid";
|
||||
local warn = require"util.prosodyctl".show_warning;
|
||||
|
||||
-- Make a *one-way* subscription. User will see when contact is online,
|
||||
-- contact will not see when user is online.
|
||||
function subscribe(user_jid, contact_jid)
|
||||
local user_username, user_host = jid.split(user_jid);
|
||||
local contact_username, contact_host = jid.split(contact_jid);
|
||||
if not hosts[user_host] then
|
||||
warn("The host '%s' is not configured for this server.", user_host);
|
||||
return;
|
||||
end
|
||||
if hosts[user_host].users.name == "null" then
|
||||
storagemanager.initialize_host(user_host);
|
||||
usermanager.initialize_host(user_host);
|
||||
end
|
||||
-- Update user's roster to say subscription request is pending. Bare hosts (e.g. components) don't have rosters.
|
||||
if user_username ~= nil then
|
||||
rostermanager.set_contact_pending_out(user_username, user_host, contact_jid);
|
||||
end
|
||||
if hosts[contact_host] then
|
||||
if contact_host ~= user_host and hosts[contact_host].users.name == "null" then
|
||||
storagemanager.initialize_host(contact_host);
|
||||
usermanager.initialize_host(contact_host);
|
||||
end
|
||||
-- Update contact's roster to say subscription request is pending...
|
||||
rostermanager.set_contact_pending_in(contact_username, contact_host, user_jid);
|
||||
-- Update contact's roster to say subscription request approved...
|
||||
rostermanager.subscribed(contact_username, contact_host, user_jid);
|
||||
-- Update user's roster to say subscription request approved. Bare hosts (e.g. components) don't have rosters.
|
||||
if user_username ~= nil then
|
||||
rostermanager.process_inbound_subscription_approval(user_username, user_host, contact_jid);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Make a mutual subscription between jid1 and jid2. Each JID will see
|
||||
-- when the other one is online.
|
||||
function subscribe_both(jid1, jid2)
|
||||
subscribe(jid1, jid2);
|
||||
subscribe(jid2, jid1);
|
||||
end
|
||||
|
||||
-- Unsubscribes user from contact (not contact from user, if subscribed).
|
||||
function unsubscribe(user_jid, contact_jid)
|
||||
local user_username, user_host = jid.split(user_jid);
|
||||
local contact_username, contact_host = jid.split(contact_jid);
|
||||
if not hosts[user_host] then
|
||||
warn("The host '%s' is not configured for this server.", user_host);
|
||||
return;
|
||||
end
|
||||
if hosts[user_host].users.name == "null" then
|
||||
storagemanager.initialize_host(user_host);
|
||||
usermanager.initialize_host(user_host);
|
||||
end
|
||||
-- Update user's roster to say subscription is cancelled...
|
||||
rostermanager.unsubscribe(user_username, user_host, contact_jid);
|
||||
if hosts[contact_host] then
|
||||
if contact_host ~= user_host and hosts[contact_host].users.name == "null" then
|
||||
storagemanager.initialize_host(contact_host);
|
||||
usermanager.initialize_host(contact_host);
|
||||
end
|
||||
-- Update contact's roster to say subscription is cancelled...
|
||||
rostermanager.unsubscribed(contact_username, contact_host, user_jid);
|
||||
end
|
||||
end
|
||||
|
||||
-- Cancel any subscription in either direction.
|
||||
function unsubscribe_both(jid1, jid2)
|
||||
unsubscribe(jid1, jid2);
|
||||
unsubscribe(jid2, jid1);
|
||||
end
|
||||
|
||||
-- Set the name shown and group used in the contact list
|
||||
function rename(user_jid, contact_jid, contact_nick, contact_group)
|
||||
local user_username, user_host = jid.split(user_jid);
|
||||
if not hosts[user_host] then
|
||||
warn("The host '%s' is not configured for this server.", user_host);
|
||||
return;
|
||||
end
|
||||
if hosts[user_host].users.name == "null" then
|
||||
storagemanager.initialize_host(user_host);
|
||||
usermanager.initialize_host(user_host);
|
||||
end
|
||||
|
||||
-- Load user's roster and find the contact
|
||||
local roster = rostermanager.load_roster(user_username, user_host);
|
||||
local item = roster[contact_jid];
|
||||
if item then
|
||||
if contact_nick then
|
||||
item.name = contact_nick;
|
||||
end
|
||||
if contact_group then
|
||||
item.groups = {}; -- Remove from all current groups
|
||||
item.groups[contact_group] = true;
|
||||
end
|
||||
rostermanager.save_roster(user_username, user_host, roster);
|
||||
end
|
||||
end
|
||||
|
||||
function remove(user_jid, contact_jid)
|
||||
unsubscribe_both(user_jid, contact_jid);
|
||||
local user_username, user_host = jid.split(user_jid);
|
||||
local roster = rostermanager.load_roster(user_username, user_host);
|
||||
roster[contact_jid] = nil;
|
||||
rostermanager.save_roster(user_username, user_host, roster);
|
||||
end
|
||||
|
||||
function module.command(arg)
|
||||
local command = arg[1];
|
||||
if not command then
|
||||
warn("Valid subcommands: (un)subscribe(_both) | rename");
|
||||
return 0;
|
||||
end
|
||||
table.remove(arg, 1);
|
||||
if command == "subscribe" then
|
||||
subscribe(arg[1], arg[2]);
|
||||
return 0;
|
||||
elseif command == "subscribe_both" then
|
||||
subscribe_both(arg[1], arg[2]);
|
||||
return 0;
|
||||
elseif command == "unsubscribe" then
|
||||
unsubscribe(arg[1], arg[2]);
|
||||
return 0;
|
||||
elseif command == "unsubscribe_both" then
|
||||
unsubscribe_both(arg[1], arg[2]);
|
||||
return 0;
|
||||
elseif command == "remove" then
|
||||
remove(arg[1], arg[2]);
|
||||
return 0;
|
||||
elseif command == "rename" then
|
||||
rename(arg[1], arg[2], arg[3], arg[4]);
|
||||
return 0;
|
||||
else
|
||||
warn("Unknown command: %s", command);
|
||||
return 1;
|
||||
end
|
||||
end
|
47
roles/jitsi/files/prosody/modules/mod_roster_command.patch
Normal file
47
roles/jitsi/files/prosody/modules/mod_roster_command.patch
Normal file
@ -0,0 +1,47 @@
|
||||
# HG changeset patch
|
||||
# User Boris Grozev <boris@jitsi.org>
|
||||
# Date 1609874100 21600
|
||||
# Tue Jan 05 13:15:00 2021 -0600
|
||||
# Node ID f646babfc401494ff33f2126ef6c4df541ebf846
|
||||
# Parent 456b9f608fcf9667cfba1bd7bf9eba2151af50d0
|
||||
mod_roster_command: Fix subscription when the "user JID" is a bare domain.
|
||||
|
||||
Do not attempt to update the roster when the user is bare domain (e.g. a
|
||||
component), since they don't have rosters and the attempt results in an error:
|
||||
|
||||
$ prosodyctl mod_roster_command subscribe proxy.example.com contact@example.com
|
||||
xxxxxxxxxxFailed to execute command: Error: /usr/lib/prosody/core/rostermanager.lua:104: attempt to concatenate local 'username' (a nil value)
|
||||
stack traceback:
|
||||
/usr/lib/prosody/core/rostermanager.lua:104: in function 'load_roster'
|
||||
/usr/lib/prosody/core/rostermanager.lua:305: in function 'set_contact_pending_out'
|
||||
mod_roster_command.lua:44: in function 'subscribe'
|
||||
|
||||
diff -r 456b9f608fcf -r f646babfc401 mod_roster_command/mod_roster_command.lua
|
||||
--- a/mod_roster_command/mod_roster_command.lua Tue Jan 05 13:49:50 2021 +0000
|
||||
+++ b/mod_roster_command/mod_roster_command.lua Tue Jan 05 13:15:00 2021 -0600
|
||||
@@ -40,8 +40,10 @@
|
||||
storagemanager.initialize_host(user_host);
|
||||
usermanager.initialize_host(user_host);
|
||||
end
|
||||
- -- Update user's roster to say subscription request is pending...
|
||||
- rostermanager.set_contact_pending_out(user_username, user_host, contact_jid);
|
||||
+ -- Update user's roster to say subscription request is pending. Bare hosts (e.g. components) don't have rosters.
|
||||
+ if user_username ~= nil then
|
||||
+ rostermanager.set_contact_pending_out(user_username, user_host, contact_jid);
|
||||
+ end
|
||||
if hosts[contact_host] then
|
||||
if contact_host ~= user_host and hosts[contact_host].users.name == "null" then
|
||||
storagemanager.initialize_host(contact_host);
|
||||
@@ -51,8 +53,10 @@
|
||||
rostermanager.set_contact_pending_in(contact_username, contact_host, user_jid);
|
||||
-- Update contact's roster to say subscription request approved...
|
||||
rostermanager.subscribed(contact_username, contact_host, user_jid);
|
||||
- -- Update user's roster to say subscription request approved...
|
||||
- rostermanager.process_inbound_subscription_approval(user_username, user_host, contact_jid);
|
||||
+ -- Update user's roster to say subscription request approved. Bare hosts (e.g. components) don't have rosters.
|
||||
+ if user_username ~= nil then
|
||||
+ rostermanager.process_inbound_subscription_approval(user_username, user_host, contact_jid);
|
||||
+ end
|
||||
end
|
||||
end
|
||||
|
26
roles/jitsi/files/prosody/modules/mod_s2s_whitelist.lua
Normal file
26
roles/jitsi/files/prosody/modules/mod_s2s_whitelist.lua
Normal file
@ -0,0 +1,26 @@
|
||||
-- Using as a base version https://hg.prosody.im/prosody-modules/file/c1a8ce147885/mod_s2s_whitelist/mod_s2s_whitelist.lua
|
||||
local st = require "util.stanza";
|
||||
|
||||
local whitelist = module:get_option_inherited_set("s2s_whitelist", {});
|
||||
|
||||
module:hook("route/remote", function (event)
|
||||
if not whitelist:contains(event.to_host) then
|
||||
-- make sure we do not send error replies for errors
|
||||
if event.stanza.attr.type == 'error' then
|
||||
module:log('debug', 'Not whitelisted destination domain for an error: %s', event.stanza);
|
||||
return true;
|
||||
end
|
||||
|
||||
module:send(st.error_reply(event.stanza, "cancel", "not-allowed", "Communication with this domain is restricted"));
|
||||
return true;
|
||||
end
|
||||
end, 100);
|
||||
|
||||
module:hook("s2s-stream-features", function (event)
|
||||
if not whitelist:contains(event.origin.from_host) then
|
||||
event.origin:close({
|
||||
condition = "policy-violation";
|
||||
text = "Communication with this domain is restricted";
|
||||
});
|
||||
end
|
||||
end, 1000);
|
20
roles/jitsi/files/prosody/modules/mod_s2sout_override.lua
Normal file
20
roles/jitsi/files/prosody/modules/mod_s2sout_override.lua
Normal file
@ -0,0 +1,20 @@
|
||||
-- Using as a base version https://hg.prosody.im/prosody-modules/file/6cf2f32dbf40/mod_s2sout_override/mod_s2sout_override.lua
|
||||
--% requires: s2sout-pre-connect-event
|
||||
|
||||
local url = require"socket.url";
|
||||
local basic_resolver = require "net.resolvers.basic";
|
||||
|
||||
local override_for = module:get_option(module.name, {}); -- map of host to "tcp://example.com:5269"
|
||||
|
||||
module:hook("s2sout-pre-connect", function(event)
|
||||
local override = override_for[event.session.to_host];
|
||||
if type(override) == "string" then
|
||||
override = url.parse(override);
|
||||
end
|
||||
if type(override) == "table" and override.scheme == "tcp" and type(override.host) == "string" then
|
||||
event.resolver = basic_resolver.new(override.host, tonumber(override.port) or 5269, override.scheme, {});
|
||||
elseif type(override) == "table" and override.scheme == "tls" and type(override.host) == "string" then
|
||||
event.resolver = basic_resolver.new(override.host, tonumber(override.port) or 5270, "tcp",
|
||||
{ servername = event.session.to_host; sslctx = event.session.ssl_ctx });
|
||||
end
|
||||
end);
|
93
roles/jitsi/files/prosody/modules/mod_s2soutinjection.lua
Normal file
93
roles/jitsi/files/prosody/modules/mod_s2soutinjection.lua
Normal file
@ -0,0 +1,93 @@
|
||||
-- Using version https://hg.prosody.im/prosody-modules/file/4fb922aa0ace/mod_s2soutinjection/mod_s2soutinjection.lua
|
||||
local st = require"util.stanza";
|
||||
local new_outgoing = require"core.s2smanager".new_outgoing;
|
||||
local bounce_sendq = module:depends"s2s".route_to_new_session.bounce_sendq;
|
||||
local initialize_filters = require "util.filters".initialize;
|
||||
|
||||
local portmanager = require "core.portmanager";
|
||||
|
||||
local addclient = require "net.server".addclient;
|
||||
|
||||
module:depends("s2s");
|
||||
|
||||
local sessions = module:shared("sessions");
|
||||
|
||||
local injected = module:get_option("s2s_connect_overrides");
|
||||
|
||||
-- The proxy_listener handles connection while still connecting to the proxy,
|
||||
-- then it hands them over to the normal listener (in mod_s2s)
|
||||
local proxy_listener = { default_port = nil, default_mode = "*a", default_interface = "*" };
|
||||
|
||||
function proxy_listener.onconnect(conn)
|
||||
local session = sessions[conn];
|
||||
|
||||
-- needed in mod_rate_limit
|
||||
session.ip = conn:ip();
|
||||
|
||||
-- Now the real s2s listener can take over the connection.
|
||||
local listener = portmanager.get_service("s2s").listener;
|
||||
|
||||
local log = session.log;
|
||||
|
||||
local filter = initialize_filters(session);
|
||||
|
||||
session.version = 1;
|
||||
|
||||
session.sends2s = function (t)
|
||||
-- log("debug", "sending (s2s over proxy): %s", (t.top_tag and t:top_tag()) or t:match("^[^>]*>?"));
|
||||
if t.name then
|
||||
t = filter("stanzas/out", t);
|
||||
end
|
||||
if t then
|
||||
t = filter("bytes/out", tostring(t));
|
||||
if t then
|
||||
return conn:write(tostring(t));
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
session.open_stream = function ()
|
||||
session.sends2s(st.stanza("stream:stream", {
|
||||
xmlns='jabber:server', ["xmlns:db"]='jabber:server:dialback',
|
||||
["xmlns:stream"]='http://etherx.jabber.org/streams',
|
||||
from=session.from_host, to=session.to_host, version='1.0', ["xml:lang"]='en'}):top_tag());
|
||||
end
|
||||
|
||||
conn.setlistener(conn, listener);
|
||||
|
||||
listener.register_outgoing(conn, session);
|
||||
|
||||
listener.onconnect(conn);
|
||||
end
|
||||
|
||||
function proxy_listener.register_outgoing(conn, session)
|
||||
session.direction = "outgoing";
|
||||
sessions[conn] = session;
|
||||
end
|
||||
|
||||
function proxy_listener.ondisconnect(conn, err)
|
||||
sessions[conn] = nil;
|
||||
end
|
||||
|
||||
module:hook("route/remote", function(event)
|
||||
local from_host, to_host, stanza = event.from_host, event.to_host, event.stanza;
|
||||
local inject = injected and injected[to_host];
|
||||
if not inject then return end
|
||||
-- module:log("debug", "opening a new outgoing connection for this stanza");
|
||||
local host_session = new_outgoing(from_host, to_host);
|
||||
|
||||
-- Store in buffer
|
||||
host_session.bounce_sendq = bounce_sendq;
|
||||
host_session.sendq = { {tostring(stanza), stanza.attr.type ~= "error" and stanza.attr.type ~= "result" and st.reply(stanza)} };
|
||||
-- host_session.log("debug", "stanza [%s] queued until connection complete", tostring(stanza.name));
|
||||
|
||||
local host, port = inject[1] or inject, tonumber(inject[2]) or 5269;
|
||||
|
||||
local conn = addclient(host, port, proxy_listener, "*a");
|
||||
|
||||
proxy_listener.register_outgoing(conn, host_session);
|
||||
|
||||
host_session.conn = conn;
|
||||
return true;
|
||||
end, -2);
|
||||
|
20
roles/jitsi/files/prosody/modules/mod_secure_interfaces.lua
Normal file
20
roles/jitsi/files/prosody/modules/mod_secure_interfaces.lua
Normal file
@ -0,0 +1,20 @@
|
||||
-- Using version https://hg.prosody.im/prosody-modules/file/6c806a99f802/mod_secure_interfaces/mod_secure_interfaces.lua
|
||||
local secure_interfaces = module:get_option_set("secure_interfaces", { "127.0.0.1", "::1" });
|
||||
|
||||
module:hook("stream-features", function (event)
|
||||
local session = event.origin;
|
||||
if session.type ~= "c2s_unauthed" then return; end
|
||||
local socket = session.conn:socket();
|
||||
if not socket.getsockname then
|
||||
module:log("debug", "Unable to determine local address of incoming connection");
|
||||
return;
|
||||
end
|
||||
local localip = socket:getsockname();
|
||||
if secure_interfaces:contains(localip) then
|
||||
-- module:log("debug", "Marking session from %s to %s as secure", session.ip or "[?]", localip);
|
||||
session.secure = true;
|
||||
session.conn.starttls = false;
|
||||
-- else
|
||||
-- module:log("debug", "Not marking session from %s to %s as secure", session.ip or "[?]", localip);
|
||||
end
|
||||
end, 2500);
|
683
roles/jitsi/files/prosody/modules/mod_smacks.lua
Normal file
683
roles/jitsi/files/prosody/modules/mod_smacks.lua
Normal file
@ -0,0 +1,683 @@
|
||||
-- XEP-0198: Stream Management for Prosody IM
|
||||
--
|
||||
-- Copyright (C) 2010-2015 Matthew Wild
|
||||
-- Copyright (C) 2010 Waqas Hussain
|
||||
-- Copyright (C) 2012-2021 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_remove = 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_mam2 = "urn:xmpp:mam:2";
|
||||
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", 600);
|
||||
local s2s_smacks = module:get_option_boolean("smacks_enabled_s2s", true);
|
||||
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 max_inactive_unacked_stanzas = module:get_option_number("smacks_max_inactive_unacked_stanzas", 256);
|
||||
local delayed_ack_timeout = module:get_option_number("smacks_max_ack_delay", 30);
|
||||
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, stanza)
|
||||
-- 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, stanza = stanza});
|
||||
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, stanza)
|
||||
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
|
||||
local max_unacked = max_unacked_stanzas;
|
||||
if session.state == "inactive" then
|
||||
max_unacked = max_inactive_unacked_stanzas;
|
||||
end
|
||||
-- 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 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 }))
|
||||
if session.destroyed then return end -- sending something can trigger destruction
|
||||
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, nil); -- we don't know if this is the only new stanza in the queue
|
||||
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, stanza); -- this is the only new stanza in the queue --> provide it to other modules
|
||||
end
|
||||
end
|
||||
|
||||
local function outgoing_stanza_filter(stanza, session)
|
||||
-- XXX: Normally you wouldn't have to check the xmlns for a stanza as it's
|
||||
-- supposed to be nil.
|
||||
-- However, when using mod_smacks with mod_websocket, then mod_websocket's
|
||||
-- stanzas/out filter can get called before this one and adds the xmlns.
|
||||
local is_stanza = stanza.attr and
|
||||
(not stanza.attr.xmlns or stanza.attr.xmlns == 'jabber:client')
|
||||
and not stanza.name:find":";
|
||||
|
||||
if is_stanza and not stanza._cached then
|
||||
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 since %s, stanza queued", datetime.datetime(session.hibernating));
|
||||
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", stanza);
|
||||
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", nil);
|
||||
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", nil)
|
||||
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
|
||||
if queue[i].attr.type ~= "error" 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
|
||||
end
|
||||
|
||||
-- don't send delivery errors for messages which will be delivered by mam later on
|
||||
-- check if stanza was 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)
|
||||
local function get_stanza_id(stanza, by_jid)
|
||||
for tag in stanza:childtags("stanza-id", "urn:xmpp:sid:0") do
|
||||
if tag.attr.by == by_jid then
|
||||
return tag.attr.id;
|
||||
end
|
||||
end
|
||||
return nil;
|
||||
end
|
||||
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
|
||||
-- don't store messages in offline store if they are mam results
|
||||
local mam_result = stanza:get_child("result", xmlns_mam2);
|
||||
if mam_result ~= nil then
|
||||
return true; -- stanza already "handled", don't send an error and don't add it to offline storage
|
||||
end
|
||||
-- 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)
|
||||
local stanza_id = get_stanza_id(stanza, jid.bare(session.full_jid));
|
||||
if session.mam_requested and stanza_id ~= nil then
|
||||
session.log("debug", "mod_smacks delivery/failure returning true for mam-handled stanza: mam-archive-id=%s", tostring(stanza_id));
|
||||
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
|
||||
local ok = module:fire_event("message/offline/handle", { origin = session, stanza = stanza } );
|
||||
session.log("debug", "mod_smacks delivery/failuere returning %s for offline-handled stanza", tostring(ok));
|
||||
return ok; -- if stanza was handled, don't send an error
|
||||
end
|
||||
end
|
||||
end
|
||||
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 occurred 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 session.push_identifier ~= nil and current_time-timeout_start < resume_timeout then
|
||||
session.log("debug", "A push happened since hibernation started, hibernating session for up to %d extra seconds", resume_timeout-(current_time-timeout_start));
|
||||
return resume_timeout-(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", nil);
|
||||
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);
|
||||
|
||||
module:hook("csi-client-active", function (event)
|
||||
if event.origin.smacks then
|
||||
request_ack_if_needed(event.origin, true, "csi-active", nil);
|
||||
end
|
||||
end);
|
||||
|
||||
module:hook("csi-flushing", function (event)
|
||||
if event.session.smacks then
|
||||
request_ack_if_needed(event.session, true, "csi-active", nil);
|
||||
end
|
||||
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, nil);
|
||||
end);
|
||||
end
|
||||
return true;
|
||||
end
|
||||
end
|
||||
|
||||
module:hook("s2s-read-timeout", handle_read_timeout);
|
||||
module:hook("c2s-read-timeout", handle_read_timeout);
|
6
roles/jitsi/files/prosody/modules/mod_speakerstats.lua
Normal file
6
roles/jitsi/files/prosody/modules/mod_speakerstats.lua
Normal file
@ -0,0 +1,6 @@
|
||||
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);
|
379
roles/jitsi/files/prosody/modules/mod_speakerstats_component.lua
Normal file
379
roles/jitsi/files/prosody/modules/mod_speakerstats_component.lua
Normal file
@ -0,0 +1,379 @@
|
||||
local util = module:require "util";
|
||||
local get_room_from_jid = util.get_room_from_jid;
|
||||
local room_jid_match_rewrite = util.room_jid_match_rewrite;
|
||||
local is_healthcheck_room = util.is_healthcheck_room;
|
||||
local process_host_module = util.process_host_module;
|
||||
local jid_resource = require "util.jid".resource;
|
||||
local st = require "util.stanza";
|
||||
local socket = require "socket";
|
||||
local json = require 'cjson.safe';
|
||||
local um_is_admin = require "core.usermanager".is_admin;
|
||||
local jid_split = require 'util.jid'.split;
|
||||
|
||||
-- we use async to detect Prosody 0.10 and earlier
|
||||
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");
|
||||
local muc_domain_base = module:get_option_string("muc_mapper_domain_base");
|
||||
|
||||
if muc_component_host == nil or muc_domain_base == nil then
|
||||
module:log("error", "No muc_component specified. No muc to operate on!");
|
||||
return;
|
||||
end
|
||||
local breakout_room_component_host = "breakout." .. muc_domain_base;
|
||||
|
||||
module:log("info", "Starting speakerstats for %s", muc_component_host);
|
||||
|
||||
local main_muc_service;
|
||||
|
||||
local function is_admin(jid)
|
||||
return um_is_admin(jid, module.host);
|
||||
end
|
||||
|
||||
-- Searches all rooms in the main muc component that holds a breakout room
|
||||
-- caches it if found so we don't search it again
|
||||
-- we should not cache objects in _data as this is being serialized when calling room:save()
|
||||
local function get_main_room(breakout_room)
|
||||
if breakout_room.main_room then
|
||||
return breakout_room.main_room;
|
||||
end
|
||||
|
||||
-- let's search all rooms to find the main room
|
||||
for room in main_muc_service.each_room() do
|
||||
if room._data and room._data.breakout_rooms_active and room._data.breakout_rooms[breakout_room.jid] then
|
||||
breakout_room.main_room = room;
|
||||
return room;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- 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 silence = speakerStats.attr.silence == 'true';
|
||||
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
|
||||
|
||||
if not room.speakerStats then
|
||||
module:log("warn", "No speakerStats found for %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
|
||||
module:log("warn", "No occupant %s found for %s", from, roomAddress);
|
||||
return false;
|
||||
end
|
||||
|
||||
local newDominantSpeaker = roomSpeakerStats[occupant.jid];
|
||||
local oldDominantSpeakerId = roomSpeakerStats['dominantSpeakerId'];
|
||||
|
||||
if oldDominantSpeakerId and occupant.jid ~= oldDominantSpeakerId then
|
||||
local oldDominantSpeaker = roomSpeakerStats[oldDominantSpeakerId];
|
||||
if oldDominantSpeaker then
|
||||
oldDominantSpeaker:setDominantSpeaker(false, false);
|
||||
end
|
||||
end
|
||||
|
||||
if newDominantSpeaker then
|
||||
newDominantSpeaker:setDominantSpeaker(true, silence);
|
||||
end
|
||||
|
||||
room.speakerStats['dominantSpeakerId'] = occupant.jid;
|
||||
end
|
||||
|
||||
local newFaceLandmarks = event.stanza:get_child('faceLandmarks', 'http://jitsi.org/jitmeet');
|
||||
|
||||
if newFaceLandmarks then
|
||||
local roomAddress = newFaceLandmarks.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
|
||||
if not room.speakerStats then
|
||||
module:log("warn", "No speakerStats found for %s", roomAddress);
|
||||
return false;
|
||||
end
|
||||
local from = event.stanza.attr.from;
|
||||
|
||||
local occupant = room:get_occupant_by_real_jid(from);
|
||||
if not occupant or not room.speakerStats[occupant.jid] then
|
||||
module:log("warn", "No occupant %s found for %s", from, roomAddress);
|
||||
return false;
|
||||
end
|
||||
local faceLandmarks = room.speakerStats[occupant.jid].faceLandmarks;
|
||||
table.insert(faceLandmarks,
|
||||
{
|
||||
faceExpression = newFaceLandmarks.attr.faceExpression,
|
||||
timestamp = tonumber(newFaceLandmarks.attr.timestamp),
|
||||
duration = tonumber(newFaceLandmarks.attr.duration),
|
||||
})
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
--- Start SpeakerStats implementation
|
||||
local SpeakerStats = {};
|
||||
SpeakerStats.__index = SpeakerStats;
|
||||
|
||||
function new_SpeakerStats(nick, context_user)
|
||||
return setmetatable({
|
||||
totalDominantSpeakerTime = 0;
|
||||
_dominantSpeakerStart = 0;
|
||||
_isSilent = false;
|
||||
_isDominantSpeaker = false;
|
||||
nick = nick;
|
||||
context_user = context_user;
|
||||
displayName = nil;
|
||||
faceLandmarks = {};
|
||||
}, 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, silence)
|
||||
-- module:log("debug", "set isDominant %s for %s", tostring(isNowDominantSpeaker), self.nick);
|
||||
|
||||
local now = socket.gettime()*1000;
|
||||
|
||||
if not self:isDominantSpeaker() and isNowDominantSpeaker and not silence then
|
||||
self._dominantSpeakerStart = now;
|
||||
elseif self:isDominantSpeaker() then
|
||||
if not isNowDominantSpeaker then
|
||||
if not self._isSilent then
|
||||
local timeElapsed = math.floor(now - self._dominantSpeakerStart);
|
||||
|
||||
self.totalDominantSpeakerTime = self.totalDominantSpeakerTime + timeElapsed;
|
||||
self._dominantSpeakerStart = 0;
|
||||
end
|
||||
elseif self._isSilent and not silence then
|
||||
self._dominantSpeakerStart = now;
|
||||
elseif not self._isSilent and silence then
|
||||
local timeElapsed = math.floor(now - self._dominantSpeakerStart);
|
||||
|
||||
self.totalDominantSpeakerTime = self.totalDominantSpeakerTime + timeElapsed;
|
||||
self._dominantSpeakerStart = 0;
|
||||
end
|
||||
end
|
||||
|
||||
self._isDominantSpeaker = isNowDominantSpeaker;
|
||||
self._isSilent = silence;
|
||||
end
|
||||
|
||||
-- Returns true if the tracked user is currently a dominant speaker.
|
||||
function SpeakerStats:isDominantSpeaker()
|
||||
return self._isDominantSpeaker;
|
||||
end
|
||||
|
||||
-- Returns true if the tracked user is currently silent.
|
||||
function SpeakerStats:isSilent()
|
||||
return self._isSilent;
|
||||
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 = {};
|
||||
room.speakerStats.sessionId = room._data.meetingId;
|
||||
end
|
||||
|
||||
-- create speakerStats for the breakout
|
||||
function breakout_room_created(event)
|
||||
local room = event.room;
|
||||
if is_healthcheck_room(room.jid) then
|
||||
return ;
|
||||
end
|
||||
local main_room = get_main_room(room);
|
||||
room.speakerStats = {};
|
||||
room.speakerStats.isBreakout = true
|
||||
room.speakerStats.breakoutRoomId = jid_split(room.jid)
|
||||
room.speakerStats.sessionId = main_room._data.meetingId;
|
||||
end
|
||||
|
||||
-- Create SpeakerStats object for the joined user
|
||||
function occupant_joined(event)
|
||||
local occupant, room = event.occupant, event.room;
|
||||
|
||||
if is_healthcheck_room(room.jid) or is_admin(occupant.bare_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 and type(values) == 'table' and values.nick ~= nil and values.nick ~= 'focus' then
|
||||
local totalDominantSpeakerTime = values.totalDominantSpeakerTime;
|
||||
local faceLandmarks = values.faceLandmarks;
|
||||
if totalDominantSpeakerTime > 0 or room:get_occupant_jid(jid) == nil or values:isDominantSpeaker()
|
||||
or next(faceLandmarks) ~= nil then
|
||||
-- before sending we need to calculate current dominant speaker state
|
||||
if values:isDominantSpeaker() and not values:isSilent() then
|
||||
local timeElapsed = math.floor(socket.gettime()*1000 - values._dominantSpeakerStart);
|
||||
totalDominantSpeakerTime = totalDominantSpeakerTime + timeElapsed;
|
||||
end
|
||||
|
||||
users_json[values.nick] = {
|
||||
displayName = values.displayName,
|
||||
totalDominantSpeakerTime = totalDominantSpeakerTime,
|
||||
faceLandmarks = faceLandmarks
|
||||
};
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if next(users_json) ~= nil then
|
||||
local body_json = {};
|
||||
body_json.type = 'speakerstats';
|
||||
body_json.users = users_json;
|
||||
|
||||
local json_msg_str, error = json.encode(body_json);
|
||||
|
||||
if json_msg_str then
|
||||
local stanza = st.message({
|
||||
from = module.host;
|
||||
to = occupant.jid; })
|
||||
:tag("json-message", {xmlns='http://jitsi.org/jitmeet'})
|
||||
:text(json_msg_str):up();
|
||||
|
||||
room:route_stanza(stanza);
|
||||
else
|
||||
module:log('error', 'Error encoding room:%s error:%s', room.jid, error);
|
||||
end
|
||||
end
|
||||
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
|
||||
|
||||
if not room.speakerStats then
|
||||
return;
|
||||
end
|
||||
|
||||
local occupant = event.occupant;
|
||||
|
||||
local speakerStatsForOccupant = room.speakerStats[occupant.jid];
|
||||
if speakerStatsForOccupant then
|
||||
speakerStatsForOccupant:setDominantSpeaker(false, 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
|
||||
|
||||
module:fire_event("send-speaker-stats", { room = room; roomSpeakerStats = room.speakerStats; });
|
||||
end
|
||||
|
||||
module:hook("message/host", on_message);
|
||||
|
||||
function process_main_muc_loaded(main_muc, host_module)
|
||||
-- the conference muc component
|
||||
module:log("info", "Hook to muc events on %s", host_module.host);
|
||||
main_muc_service = main_muc;
|
||||
module:log("info", "Main muc service %s", main_muc_service)
|
||||
host_module:hook("muc-room-created", room_created, -1);
|
||||
host_module:hook("muc-occupant-joined", occupant_joined, -1);
|
||||
host_module:hook("muc-occupant-pre-leave", occupant_leaving, -1);
|
||||
host_module:hook("muc-room-destroyed", room_destroyed, -1);
|
||||
end
|
||||
|
||||
function process_breakout_muc_loaded(breakout_muc, host_module)
|
||||
-- the Breakout muc component
|
||||
module:log("info", "Hook to muc events on %s", host_module.host);
|
||||
host_module:hook("muc-room-created", breakout_room_created, -1);
|
||||
host_module:hook("muc-occupant-joined", occupant_joined, -1);
|
||||
host_module:hook("muc-occupant-pre-leave", occupant_leaving, -1);
|
||||
host_module:hook("muc-room-destroyed", room_destroyed, -1);
|
||||
end
|
||||
|
||||
-- process or waits to process the conference muc component
|
||||
process_host_module(muc_component_host, function(host_module, host)
|
||||
module:log('info', 'Conference component loaded %s', host);
|
||||
|
||||
local muc_module = prosody.hosts[host].modules.muc;
|
||||
if muc_module then
|
||||
process_main_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_main_muc_loaded(prosody.hosts[host].modules.muc, host_module);
|
||||
end
|
||||
end);
|
||||
end
|
||||
end);
|
||||
|
||||
-- process or waits to process the breakout rooms muc component
|
||||
process_host_module(breakout_room_component_host, function(host_module, host)
|
||||
module:log('info', 'Breakout component loaded %s', host);
|
||||
|
||||
local muc_module = prosody.hosts[host].modules.muc;
|
||||
if muc_module then
|
||||
process_breakout_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_breakout_muc_loaded(prosody.hosts[host].modules.muc, host_module);
|
||||
end
|
||||
end);
|
||||
end
|
||||
end);
|
126
roles/jitsi/files/prosody/modules/mod_system_chat_message.lua
Normal file
126
roles/jitsi/files/prosody/modules/mod_system_chat_message.lua
Normal file
@ -0,0 +1,126 @@
|
||||
-- Module which can be used as an http endpoint to send system private chat messages to meeting participants. The provided token
|
||||
--- in the request is verified whether it has the right to do so. This module should be loaded under the virtual host.
|
||||
-- Copyright (C) 2024-present 8x8, Inc.
|
||||
|
||||
-- curl https://{host}/send-system-chat-message -d '{"message": "testmessage", "connectionJIDs": ["{connection_jid}"], "room": "{room_jid}"}' -H "content-type: application/json" -H "authorization: Bearer {token}"
|
||||
|
||||
local util = module:require "util";
|
||||
local token_util = module:require "token/util".new(module);
|
||||
|
||||
local async_handler_wrapper = util.async_handler_wrapper;
|
||||
local room_jid_match_rewrite = util.room_jid_match_rewrite;
|
||||
local starts_with = util.starts_with;
|
||||
local get_room_from_jid = util.get_room_from_jid;
|
||||
|
||||
local st = require "util.stanza";
|
||||
local json = require "cjson.safe";
|
||||
|
||||
local muc_domain_base = module:get_option_string("muc_mapper_domain_base");
|
||||
local asapKeyServer = module:get_option_string("prosody_password_public_key_repo_url", "");
|
||||
|
||||
if asapKeyServer then
|
||||
-- init token util with our asap keyserver
|
||||
token_util:set_asap_key_server(asapKeyServer)
|
||||
end
|
||||
|
||||
function verify_token(token)
|
||||
if token == nil then
|
||||
module:log("warn", "no token provided");
|
||||
return false;
|
||||
end
|
||||
|
||||
local session = {};
|
||||
session.auth_token = token;
|
||||
local verified, reason, msg = token_util:process_and_verify_token(session);
|
||||
if not verified then
|
||||
module:log("warn", "not a valid token %s %s", tostring(reason), tostring(msg));
|
||||
return false;
|
||||
end
|
||||
return true;
|
||||
end
|
||||
|
||||
function handle_send_system_message (event)
|
||||
local request = event.request;
|
||||
|
||||
module:log("debug", "Request for sending a system message received: reqid %s", request.headers["request_id"])
|
||||
|
||||
-- verify payload
|
||||
if request.headers.content_type ~= "application/json"
|
||||
or (not request.body or #request.body == 0) then
|
||||
module:log("error", "Wrong content type: %s or missing payload", request.headers.content_type);
|
||||
return { status_code = 400; }
|
||||
end
|
||||
|
||||
local payload = json.decode(request.body);
|
||||
|
||||
if not payload then
|
||||
module:log("error", "Request body is missing");
|
||||
return { status_code = 400; }
|
||||
end
|
||||
|
||||
local displayName = payload["displayName"];
|
||||
local message = payload["message"];
|
||||
local connectionJIDs = payload["connectionJIDs"];
|
||||
local payload_room = payload["room"];
|
||||
|
||||
if not message or not connectionJIDs or not payload_room then
|
||||
module:log("error", "One of [message, connectionJIDs, room] was not provided");
|
||||
return { status_code = 400; }
|
||||
end
|
||||
|
||||
local room_jid = room_jid_match_rewrite(payload_room);
|
||||
local room = get_room_from_jid(room_jid);
|
||||
|
||||
if not room then
|
||||
module:log("error", "Room %s not found", room_jid);
|
||||
return { status_code = 404; }
|
||||
end
|
||||
|
||||
-- verify access
|
||||
local token = request.headers["authorization"]
|
||||
if not token then
|
||||
module:log("error", "Authorization header was not provided for conference %s", room_jid)
|
||||
return { status_code = 401 };
|
||||
end
|
||||
if starts_with(token, 'Bearer ') then
|
||||
token = token:sub(8, #token)
|
||||
else
|
||||
module:log("error", "Authorization header is invalid")
|
||||
return { status_code = 401 };
|
||||
end
|
||||
|
||||
if not verify_token(token, room_jid) then
|
||||
return { status_code = 401 };
|
||||
end
|
||||
|
||||
local data = {
|
||||
displayName = displayName,
|
||||
type = "system_chat_message",
|
||||
message = message,
|
||||
};
|
||||
|
||||
for _, to in ipairs(connectionJIDs) do
|
||||
local stanza = st.message({
|
||||
from = room.jid,
|
||||
to = to
|
||||
})
|
||||
:tag('json-message', { xmlns = 'http://jitsi.org/jitmeet' })
|
||||
:text(json.encode(data))
|
||||
:up();
|
||||
|
||||
room:route_stanza(stanza);
|
||||
end
|
||||
|
||||
return { status_code = 200 };
|
||||
end
|
||||
|
||||
module:log("info", "Adding http handler for /send-system-chat-message on %s", module.host);
|
||||
module:depends("http");
|
||||
module:provides("http", {
|
||||
default_path = "/";
|
||||
route = {
|
||||
["POST send-system-chat-message"] = function(event)
|
||||
return async_handler_wrapper(event, handle_send_system_message)
|
||||
end;
|
||||
};
|
||||
});
|
139
roles/jitsi/files/prosody/modules/mod_token_verification.lua
Normal file
139
roles/jitsi/files/prosody/modules/mod_token_verification.lua
Normal file
@ -0,0 +1,139 @@
|
||||
-- Token authentication
|
||||
-- Copyright (C) 2021-present 8x8, Inc.
|
||||
|
||||
local log = module._log;
|
||||
local host = module.host;
|
||||
local st = require "util.stanza";
|
||||
local um_is_admin = require "core.usermanager".is_admin;
|
||||
local jid_split = require 'util.jid'.split;
|
||||
local jid_bare = require 'util.jid'.bare;
|
||||
|
||||
local DEBUG = false;
|
||||
|
||||
local measure_success = module:measure('success', 'counter');
|
||||
local measure_fail = module:measure('fail', 'counter');
|
||||
|
||||
local function is_admin(jid)
|
||||
return um_is_admin(jid, host);
|
||||
end
|
||||
|
||||
local parentHostName = string.gmatch(tostring(host), "%w+.(%w.+)")();
|
||||
if parentHostName == nil then
|
||||
module:log("error", "Failed to start - unable to get parent hostname");
|
||||
return;
|
||||
end
|
||||
|
||||
local parentCtx = module:context(parentHostName);
|
||||
if parentCtx == nil then
|
||||
module: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
|
||||
|
||||
module: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;
|
||||
-- option to allow domains to skip token verification
|
||||
local allowlist;
|
||||
local function load_config()
|
||||
require_token_for_moderation = module:get_option_boolean("token_verification_require_token_for_moderation");
|
||||
allowlist = module:get_option_set('token_verification_allowlist', {});
|
||||
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)
|
||||
if DEBUG then
|
||||
module:log("debug", "Session token: %s, session room: %s",
|
||||
tostring(session.auth_token), tostring(session.jitsi_meet_room));
|
||||
end
|
||||
|
||||
-- token not required for admin users
|
||||
local user_jid = stanza.attr.from;
|
||||
if is_admin(user_jid) then
|
||||
if DEBUG then module:log("debug", "Token not required from admin user: %s", user_jid); end
|
||||
return true;
|
||||
end
|
||||
|
||||
-- token not required for users matching allow list
|
||||
local user_bare_jid = jid_bare(user_jid);
|
||||
local _, user_domain = jid_split(user_jid);
|
||||
|
||||
-- allowlist for participants
|
||||
if allowlist:contains(user_domain) or allowlist:contains(user_bare_jid) then
|
||||
if DEBUG then module:log("debug", "Token not required from user in allow list: %s", user_jid); end
|
||||
return true;
|
||||
end
|
||||
|
||||
if DEBUG then module:log("debug", "Will verify token for user: %s, room: %s ", user_jid, stanza.attr.to); end
|
||||
if not token_util:verify_room(session, stanza.attr.to) then
|
||||
module: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
|
||||
if DEBUG then module:log("debug", "allowed: %s to enter/create room: %s", user_jid, stanza.attr.to); end
|
||||
return true;
|
||||
end
|
||||
|
||||
module:hook("muc-room-pre-create", function(event)
|
||||
local origin, stanza = event.origin, event.stanza;
|
||||
if DEBUG then module:log("debug", "pre create: %s %s", tostring(origin), tostring(stanza)); end
|
||||
if not verify_user(origin, stanza) then
|
||||
measure_fail(1);
|
||||
return true; -- Returning any value other than nil will halt processing of the event
|
||||
end
|
||||
measure_success(1);
|
||||
end, 99);
|
||||
|
||||
module:hook("muc-occupant-pre-join", function(event)
|
||||
local origin, room, stanza = event.origin, event.room, event.stanza;
|
||||
if DEBUG then module:log("debug", "pre join: %s %s", tostring(room), tostring(stanza)); end
|
||||
if not verify_user(origin, stanza) then
|
||||
measure_fail(1);
|
||||
return true; -- Returning any value other than nil will halt processing of the event
|
||||
end
|
||||
measure_success(1);
|
||||
end, 99);
|
||||
|
||||
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);
|
80
roles/jitsi/files/prosody/modules/mod_turncredentials.lua
Normal file
80
roles/jitsi/files/prosody/modules/mod_turncredentials.lua
Normal file
@ -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,31 @@
|
||||
-- http endpoint to expose turn credentials for other services
|
||||
-- Copyright (C) 2023-present 8x8, Inc.
|
||||
|
||||
local ext_services = module:depends("external_services");
|
||||
local get_services = ext_services.get_services;
|
||||
|
||||
local async_handler_wrapper = module:require "util".async_handler_wrapper;
|
||||
local json = require 'cjson.safe';
|
||||
|
||||
--- Handles request for retrieving turn credentials
|
||||
-- @param event the http event, holds the request query
|
||||
-- @return GET response, containing a json with participants details
|
||||
function handle_get_turn_credentials (event)
|
||||
local GET_response = {
|
||||
headers = {
|
||||
content_type = "application/json";
|
||||
};
|
||||
body = json.encode(get_services());
|
||||
};
|
||||
return GET_response;
|
||||
end;
|
||||
|
||||
function module.load()
|
||||
module:depends("http");
|
||||
module:provides("http", {
|
||||
default_path = "/";
|
||||
route = {
|
||||
["GET turn-credentials"] = function (event) return async_handler_wrapper(event,handle_get_turn_credentials) end;
|
||||
};
|
||||
});
|
||||
end
|
355
roles/jitsi/files/prosody/modules/mod_visitors.lua
Normal file
355
roles/jitsi/files/prosody/modules/mod_visitors.lua
Normal file
@ -0,0 +1,355 @@
|
||||
--- activate under main vhost
|
||||
--- In /etc/hosts add:
|
||||
--- vm1-ip-address visitors1.domain.com
|
||||
--- vm1-ip-address conference.visitors1.domain.com
|
||||
--- vm2-ip-address visitors2.domain.com
|
||||
--- vm2-ip-address conference.visitors2.domain.com
|
||||
--- Enable in global modules: 's2s_bidi' and 'certs_all'
|
||||
--- Make sure 's2s' is not in modules_disabled
|
||||
--- Open port 5269 on the provider side and on the firewall on the machine (iptables -I INPUT 4 -p tcp -m tcp --dport 5269 -j ACCEPT)
|
||||
--- NOTE: Make sure all communication between prosodies is using the real jids ([foo]room1@muc.example.com)
|
||||
local st = require 'util.stanza';
|
||||
local jid = require 'util.jid';
|
||||
local new_id = require 'util.id'.medium;
|
||||
local util = module:require 'util';
|
||||
local presence_check_status = util.presence_check_status;
|
||||
local process_host_module = util.process_host_module;
|
||||
|
||||
local um_is_admin = require 'core.usermanager'.is_admin;
|
||||
local function is_admin(jid)
|
||||
return um_is_admin(jid, module.host);
|
||||
end
|
||||
|
||||
local MUC_NS = 'http://jabber.org/protocol/muc';
|
||||
|
||||
-- required parameter for custom muc component prefix, defaults to 'conference'
|
||||
local muc_domain_prefix = module:get_option_string('muc_mapper_domain_prefix', 'conference');
|
||||
|
||||
local main_muc_component_config = module:get_option_string('main_muc');
|
||||
if main_muc_component_config == nil then
|
||||
module:log('error', 'visitors rooms not enabled missing main_muc config');
|
||||
return ;
|
||||
end
|
||||
|
||||
-- A list of domains which to be ignored for visitors. For occupants using those domain we do not propagate them
|
||||
-- to visitor nodes and we do not update them with presence changes
|
||||
local ignore_list = module:get_option_set('visitors_ignore_list', {});
|
||||
|
||||
-- Advertise the component for discovery via disco#items
|
||||
module:add_identity('component', 'visitors', 'visitors.'..module.host);
|
||||
|
||||
local sent_iq_cache = require 'util.cache'.new(200);
|
||||
|
||||
-- visitors_nodes = {
|
||||
-- roomjid1 = {
|
||||
-- nodes = {
|
||||
-- ['conference.visitors1.jid'] = 2, // number of main participants, on 0 we clean it
|
||||
-- ['conference.visitors2.jid'] = 3
|
||||
-- }
|
||||
-- },
|
||||
-- roomjid2 = {}
|
||||
--}
|
||||
local visitors_nodes = {};
|
||||
|
||||
-- sends connect or update iq
|
||||
-- @parameter type - Type of iq to send 'connect' or 'update'
|
||||
local function send_visitors_iq(conference_service, room, type)
|
||||
-- send iq informing the vnode that the connect is done and it will allow visitors to join
|
||||
local iq_id = new_id();
|
||||
sent_iq_cache:set(iq_id, socket.gettime());
|
||||
local connect_done = st.iq({
|
||||
type = 'set',
|
||||
to = conference_service,
|
||||
from = module.host,
|
||||
id = iq_id })
|
||||
:tag('visitors', { xmlns = 'jitsi:visitors',
|
||||
room = jid.join(jid.node(room.jid), conference_service) })
|
||||
:tag(type, { xmlns = 'jitsi:visitors',
|
||||
password = type ~= 'disconnect' and room:get_password() or '',
|
||||
lobby = room._data.lobbyroom and 'true' or 'false',
|
||||
meetingId = room._data.meetingId,
|
||||
moderatorId = room._data.moderator_id, -- can be used from external modules to set single moderator for meetings
|
||||
createdTimestamp = room.created_timestamp and tostring(room.created_timestamp) or nil
|
||||
}):up();
|
||||
|
||||
module:send(connect_done);
|
||||
end
|
||||
|
||||
-- an event received from visitors component, which receives iqs from jicofo
|
||||
local function connect_vnode(event)
|
||||
local room, vnode = event.room, event.vnode;
|
||||
local conference_service = muc_domain_prefix..'.'..vnode..'.meet.jitsi';
|
||||
|
||||
if visitors_nodes[room.jid] and
|
||||
visitors_nodes[room.jid].nodes and
|
||||
visitors_nodes[room.jid].nodes[conference_service] then
|
||||
-- nothing to do
|
||||
return;
|
||||
end
|
||||
|
||||
if visitors_nodes[room.jid] == nil then
|
||||
visitors_nodes[room.jid] = {};
|
||||
end
|
||||
if visitors_nodes[room.jid].nodes == nil then
|
||||
visitors_nodes[room.jid].nodes = {};
|
||||
end
|
||||
|
||||
local sent_main_participants = 0;
|
||||
|
||||
for _, o in room:each_occupant() do
|
||||
if not is_admin(o.bare_jid) then
|
||||
local fmuc_pr = st.clone(o:get_presence());
|
||||
local user, _, res = jid.split(o.nick);
|
||||
fmuc_pr.attr.to = jid.join(user, conference_service , res);
|
||||
fmuc_pr.attr.from = o.jid;
|
||||
-- add <x>
|
||||
fmuc_pr:tag('x', { xmlns = MUC_NS });
|
||||
|
||||
-- if there is a password on the main room let's add the password for the vnode join
|
||||
-- as we will set the password to the vnode room and we will need it
|
||||
local pass = room:get_password();
|
||||
if pass and pass ~= '' then
|
||||
fmuc_pr:tag('password'):text(pass);
|
||||
end
|
||||
fmuc_pr:up();
|
||||
|
||||
module:send(fmuc_pr);
|
||||
|
||||
sent_main_participants = sent_main_participants + 1;
|
||||
end
|
||||
end
|
||||
visitors_nodes[room.jid].nodes[conference_service] = sent_main_participants;
|
||||
|
||||
send_visitors_iq(conference_service, room, 'connect');
|
||||
end
|
||||
module:hook('jitsi-connect-vnode', connect_vnode);
|
||||
|
||||
-- listens for responses to the iq sent for connecting vnode
|
||||
local function stanza_handler(event)
|
||||
local origin, stanza = event.origin, event.stanza;
|
||||
|
||||
if stanza.name ~= 'iq' then
|
||||
return;
|
||||
end
|
||||
|
||||
-- we receive error from vnode for our disconnect message as the room was already destroyed (all visitors left)
|
||||
if (stanza.attr.type == 'result' or stanza.attr.type == 'error') and sent_iq_cache:get(stanza.attr.id) then
|
||||
sent_iq_cache:set(stanza.attr.id, nil);
|
||||
return true;
|
||||
end
|
||||
end
|
||||
module:hook('iq/host', stanza_handler, 10);
|
||||
|
||||
-- an event received from visitors component, which receives iqs from jicofo
|
||||
local function disconnect_vnode(event)
|
||||
local room, vnode = event.room, event.vnode;
|
||||
|
||||
if visitors_nodes[event.room.jid] == nil then
|
||||
-- maybe the room was already destroyed and vnodes cleared
|
||||
return;
|
||||
end
|
||||
|
||||
local conference_service = muc_domain_prefix..'.'..vnode..'.meet.jitsi';
|
||||
|
||||
visitors_nodes[room.jid].nodes[conference_service] = nil;
|
||||
|
||||
send_visitors_iq(conference_service, room, 'disconnect');
|
||||
end
|
||||
module:hook('jitsi-disconnect-vnode', disconnect_vnode);
|
||||
|
||||
-- takes care when the visitor nodes destroys the room to count the leaving participants from there, and if its really destroyed
|
||||
-- we clean up, so if we establish again the connection to the same visitor node to send the main participants
|
||||
module:hook('presence/full', function(event)
|
||||
local stanza = event.stanza;
|
||||
local room_name, from_host = jid.split(stanza.attr.from);
|
||||
if stanza.attr.type == 'unavailable' and from_host ~= main_muc_component_config then
|
||||
local room_jid = jid.join(room_name, main_muc_component_config); -- converts from visitor to main room jid
|
||||
|
||||
local x = stanza:get_child('x', 'http://jabber.org/protocol/muc#user');
|
||||
if not presence_check_status(x, '110') then
|
||||
return;
|
||||
end
|
||||
|
||||
if visitors_nodes[room_jid] and visitors_nodes[room_jid].nodes
|
||||
and visitors_nodes[room_jid].nodes[from_host] then
|
||||
visitors_nodes[room_jid].nodes[from_host] = visitors_nodes[room_jid].nodes[from_host] - 1;
|
||||
-- we clean only on disconnect coming from jicofo
|
||||
end
|
||||
end
|
||||
end, 900);
|
||||
|
||||
process_host_module(main_muc_component_config, function(host_module, host)
|
||||
-- detects presence change in a main participant and propagate it to the used visitor nodes
|
||||
host_module:hook('muc-occupant-pre-change', function (event)
|
||||
local room, stanza, occupant = event.room, event.stanza, event.dest_occupant;
|
||||
|
||||
-- filter focus and configured domains (used for jibri and transcribers)
|
||||
if is_admin(stanza.attr.from) or visitors_nodes[room.jid] == nil
|
||||
or ignore_list:contains(jid.host(occupant.bare_jid)) then
|
||||
return;
|
||||
end
|
||||
|
||||
local vnodes = visitors_nodes[room.jid].nodes;
|
||||
local user, _, res = jid.split(occupant.nick);
|
||||
-- a change in the presence of a main participant we need to update all active visitor nodes
|
||||
for k in pairs(vnodes) do
|
||||
local fmuc_pr = st.clone(stanza);
|
||||
fmuc_pr.attr.to = jid.join(user, k, res);
|
||||
fmuc_pr.attr.from = occupant.jid;
|
||||
module:send(fmuc_pr);
|
||||
end
|
||||
end);
|
||||
|
||||
-- when a main participant leaves inform the visitor nodes
|
||||
host_module:hook('muc-occupant-left', function (event)
|
||||
local room, stanza, occupant = event.room, event.stanza, event.occupant;
|
||||
|
||||
-- ignore configured domains (jibri and transcribers)
|
||||
if is_admin(occupant.bare_jid) or visitors_nodes[room.jid] == nil or visitors_nodes[room.jid].nodes == nil
|
||||
or ignore_list:contains(jid.host(occupant.bare_jid)) then
|
||||
return;
|
||||
end
|
||||
|
||||
--this is probably participant kick scenario, create an unavailable presence and send to vnodes.
|
||||
if not stanza then
|
||||
stanza = st.presence {from = occupant.nick; type = "unavailable";};
|
||||
end
|
||||
|
||||
-- we want to update visitor node that a main participant left or kicked.
|
||||
if stanza then
|
||||
local vnodes = visitors_nodes[room.jid].nodes;
|
||||
local user, _, res = jid.split(occupant.nick);
|
||||
for k in pairs(vnodes) do
|
||||
local fmuc_pr = st.clone(stanza);
|
||||
fmuc_pr.attr.to = jid.join(user, k, res);
|
||||
fmuc_pr.attr.from = occupant.jid;
|
||||
module:send(fmuc_pr);
|
||||
end
|
||||
end
|
||||
end);
|
||||
|
||||
-- cleanup cache
|
||||
host_module:hook('muc-room-destroyed',function(event)
|
||||
local room = event.room;
|
||||
|
||||
-- room is destroyed let's disconnect all vnodes
|
||||
if visitors_nodes[room.jid] then
|
||||
local vnodes = visitors_nodes[room.jid].nodes;
|
||||
for conference_service in pairs(vnodes) do
|
||||
send_visitors_iq(conference_service, room, 'disconnect');
|
||||
end
|
||||
|
||||
visitors_nodes[room.jid] = nil;
|
||||
end
|
||||
end);
|
||||
|
||||
-- detects new participants joining main room and sending them to the visitor nodes
|
||||
host_module:hook('muc-occupant-joined', function (event)
|
||||
local room, stanza, occupant = event.room, event.stanza, event.occupant;
|
||||
|
||||
-- filter focus, ignore configured domains (jibri and transcribers)
|
||||
if is_admin(stanza.attr.from) or visitors_nodes[room.jid] == nil
|
||||
or ignore_list:contains(jid.host(occupant.bare_jid)) then
|
||||
return;
|
||||
end
|
||||
|
||||
local vnodes = visitors_nodes[room.jid].nodes;
|
||||
local user, _, res = jid.split(occupant.nick);
|
||||
-- a main participant we need to update all active visitor nodes
|
||||
for k in pairs(vnodes) do
|
||||
local fmuc_pr = st.clone(stanza);
|
||||
fmuc_pr.attr.to = jid.join(user, k, res);
|
||||
fmuc_pr.attr.from = occupant.jid;
|
||||
module:send(fmuc_pr);
|
||||
end
|
||||
end);
|
||||
-- forwards messages from main participants to vnodes
|
||||
host_module:hook('muc-occupant-groupchat', function(event)
|
||||
local room, stanza, occupant = event.room, event.stanza, event.occupant;
|
||||
|
||||
-- filter sending messages from transcribers/jibris to visitors
|
||||
if not visitors_nodes[room.jid] or ignore_list:contains(jid.host(occupant.bare_jid)) then
|
||||
return;
|
||||
end
|
||||
|
||||
local vnodes = visitors_nodes[room.jid].nodes;
|
||||
local user = jid.node(occupant.nick);
|
||||
-- a main participant we need to update all active visitor nodes
|
||||
for k in pairs(vnodes) do
|
||||
local fmuc_msg = st.clone(stanza);
|
||||
fmuc_msg.attr.to = jid.join(user, k);
|
||||
fmuc_msg.attr.from = occupant.jid;
|
||||
module:send(fmuc_msg);
|
||||
end
|
||||
end);
|
||||
-- receiving messages from visitor nodes and forward them to local main participants
|
||||
-- and forward them to the rest of visitor nodes
|
||||
host_module:hook('muc-occupant-groupchat', function(event)
|
||||
local occupant, room, stanza = event.occupant, event.room, event.stanza;
|
||||
local to = stanza.attr.to;
|
||||
local from = stanza.attr.from;
|
||||
local from_vnode = jid.host(from);
|
||||
|
||||
if occupant or not (visitors_nodes[to]
|
||||
and visitors_nodes[to].nodes
|
||||
and visitors_nodes[to].nodes[from_vnode]) then
|
||||
return;
|
||||
end
|
||||
|
||||
-- a message from visitor occupant of known visitor node
|
||||
stanza.attr.from = to;
|
||||
for _, o in room:each_occupant() do
|
||||
-- send it to the nick to be able to route it to the room (ljm multiple rooms) from unknown occupant
|
||||
room:route_to_occupant(o, stanza);
|
||||
end
|
||||
-- let's add the message to the history of the room
|
||||
host_module:fire_event("muc-add-history", { room = room; stanza = stanza; from = from; visitor = true; });
|
||||
|
||||
-- now we need to send to rest of visitor nodes
|
||||
local vnodes = visitors_nodes[room.jid].nodes;
|
||||
for k in pairs(vnodes) do
|
||||
if k ~= from_vnode then
|
||||
local st_copy = st.clone(stanza);
|
||||
st_copy.attr.to = jid.join(jid.node(room.jid), k);
|
||||
module:send(st_copy);
|
||||
end
|
||||
end
|
||||
|
||||
return true;
|
||||
end, 55); -- prosody check for unknown participant chat is prio 50, we want to override it
|
||||
|
||||
host_module:hook('muc-config-submitted/muc#roomconfig_roomsecret', function(event)
|
||||
if event.status_codes['104'] then
|
||||
local room = event.room;
|
||||
|
||||
if visitors_nodes[room.jid] then
|
||||
-- we need to update all vnodes
|
||||
local vnodes = visitors_nodes[room.jid].nodes;
|
||||
for conference_service in pairs(vnodes) do
|
||||
send_visitors_iq(conference_service, room, 'update');
|
||||
end
|
||||
end
|
||||
end
|
||||
end, -100); -- we want to run last in order to check is the status code 104
|
||||
end);
|
||||
|
||||
module:hook('jitsi-lobby-enabled', function(event)
|
||||
local room = event.room;
|
||||
if visitors_nodes[room.jid] then
|
||||
-- we need to update all vnodes
|
||||
local vnodes = visitors_nodes[room.jid].nodes;
|
||||
for conference_service in pairs(vnodes) do
|
||||
send_visitors_iq(conference_service, room, 'update');
|
||||
end
|
||||
end
|
||||
end);
|
||||
module:hook('jitsi-lobby-disabled', function(event)
|
||||
local room = event.room;
|
||||
if visitors_nodes[room.jid] then
|
||||
-- we need to update all vnodes
|
||||
local vnodes = visitors_nodes[room.jid].nodes;
|
||||
for conference_service in pairs(vnodes) do
|
||||
send_visitors_iq(conference_service, room, 'update');
|
||||
end
|
||||
end
|
||||
end);
|
573
roles/jitsi/files/prosody/modules/mod_visitors_component.lua
Normal file
573
roles/jitsi/files/prosody/modules/mod_visitors_component.lua
Normal file
@ -0,0 +1,573 @@
|
||||
module:log('info', 'Starting visitors_component at %s', module.host);
|
||||
|
||||
local http = require 'net.http';
|
||||
local jid = require 'util.jid';
|
||||
local st = require 'util.stanza';
|
||||
local util = module:require 'util';
|
||||
local is_healthcheck_room = util.is_healthcheck_room;
|
||||
local is_sip_jigasi = util.is_sip_jigasi;
|
||||
local room_jid_match_rewrite = util.room_jid_match_rewrite;
|
||||
local get_room_from_jid = util.get_room_from_jid;
|
||||
local get_focus_occupant = util.get_focus_occupant;
|
||||
local get_room_by_name_and_subdomain = util.get_room_by_name_and_subdomain;
|
||||
local internal_room_jid_match_rewrite = util.internal_room_jid_match_rewrite;
|
||||
local is_vpaas = util.is_vpaas;
|
||||
local is_sip_jibri_join = util.is_sip_jibri_join;
|
||||
local process_host_module = util.process_host_module;
|
||||
local new_id = require 'util.id'.medium;
|
||||
local um_is_admin = require 'core.usermanager'.is_admin;
|
||||
local json = require 'cjson.safe';
|
||||
local inspect = require 'inspect';
|
||||
|
||||
-- will be initialized once the main virtual host module is initialized
|
||||
local token_util;
|
||||
|
||||
local MUC_NS = 'http://jabber.org/protocol/muc';
|
||||
|
||||
local muc_domain_prefix = module:get_option_string('muc_mapper_domain_prefix', 'conference');
|
||||
local muc_domain_base = module:get_option_string('muc_mapper_domain_base');
|
||||
if not muc_domain_base then
|
||||
module:log('warn', 'No muc_domain_base option set.');
|
||||
return;
|
||||
end
|
||||
|
||||
-- A list of domains which to be ignored for visitors. The config is set under the main virtual host
|
||||
local ignore_list = module:context(muc_domain_base):get_option_set('visitors_ignore_list', {});
|
||||
|
||||
local auto_allow_promotion = module:get_option_boolean('auto_allow_visitor_promotion', false);
|
||||
|
||||
-- whether to always advertise that visitors feature is enabled for rooms
|
||||
-- can be set to off and being controlled by another module, turning it on and off for rooms
|
||||
local always_visitors_enabled = module:get_option_boolean('always_visitors_enabled', true);
|
||||
|
||||
local visitors_queue_service = module:get_option_string('visitors_queue_service');
|
||||
local http_headers = {
|
||||
["User-Agent"] = "Prosody (" .. prosody.version .. "; " .. prosody.platform .. ")",
|
||||
["Content-Type"] = "application/json",
|
||||
["Accept"] = "application/json"
|
||||
};
|
||||
|
||||
local function is_admin(jid)
|
||||
return um_is_admin(jid, module.host);
|
||||
end
|
||||
|
||||
-- This is a map to keep data for room and the jids that were allowed to join after visitor mode is enabled
|
||||
-- automatically allowed or allowed by a moderator
|
||||
local visitors_promotion_map = {};
|
||||
|
||||
-- A map with key room jid. The content is a map with key jid from which the request is received
|
||||
-- and the value is a table that has the json message that needs to be sent to any future moderator that joins
|
||||
-- and the vnode from which the request is received and where the response will be sent
|
||||
local visitors_promotion_requests = {};
|
||||
|
||||
local cache = require 'util.cache';
|
||||
local sent_iq_cache = cache.new(200);
|
||||
|
||||
-- send iq result that the iq was received and will be processed
|
||||
local function respond_iq_result(origin, stanza)
|
||||
-- respond with successful receiving the iq
|
||||
origin.send(st.iq({
|
||||
type = 'result';
|
||||
from = stanza.attr.to;
|
||||
to = stanza.attr.from;
|
||||
id = stanza.attr.id
|
||||
}));
|
||||
end
|
||||
|
||||
-- Sends a json-message to the destination jid
|
||||
-- @param to_jid the destination jid
|
||||
-- @param json_message the message content to send
|
||||
function send_json_message(to_jid, json_message)
|
||||
local stanza = st.message({ from = module.host; to = to_jid; })
|
||||
:tag('json-message', { xmlns = 'http://jitsi.org/jitmeet' }):text(json_message):up();
|
||||
module:send(stanza);
|
||||
end
|
||||
|
||||
local function request_promotion_received(room, from_jid, from_vnode, nick, time, user_id, force_promote)
|
||||
-- if visitors is enabled for the room
|
||||
if visitors_promotion_map[room.jid] then
|
||||
-- only for raise hand, ignore lowering the hand
|
||||
if time and time > 0 and (
|
||||
auto_allow_promotion
|
||||
or force_promote == 'true') then
|
||||
-- we are in auto-allow mode, let's reply with accept
|
||||
-- we store where the request is coming from so we can send back the response
|
||||
local username = new_id():lower();
|
||||
visitors_promotion_map[room.jid][username] = {
|
||||
from = from_vnode;
|
||||
jid = from_jid;
|
||||
};
|
||||
|
||||
local req_from = visitors_promotion_map[room.jid][username].from;
|
||||
local req_jid = visitors_promotion_map[room.jid][username].jid;
|
||||
local focus_occupant = get_focus_occupant(room);
|
||||
local focus_jid = focus_occupant and focus_occupant.bare_jid or nil;
|
||||
|
||||
local iq_id = new_id();
|
||||
sent_iq_cache:set(iq_id, socket.gettime());
|
||||
|
||||
local node = jid.node(room.jid);
|
||||
|
||||
module:send(st.iq({
|
||||
type='set', to = req_from, from = module.host, id = iq_id })
|
||||
:tag('visitors', {
|
||||
xmlns='jitsi:visitors',
|
||||
room = jid.join(node, muc_domain_prefix..'.'..req_from),
|
||||
focusjid = focus_jid })
|
||||
:tag('promotion-response', {
|
||||
xmlns='jitsi:visitors',
|
||||
jid = req_jid,
|
||||
username = username ,
|
||||
allow = 'true' }):up());
|
||||
return true;
|
||||
else
|
||||
-- send promotion request to all moderators
|
||||
local body_json = {};
|
||||
body_json.type = 'visitors';
|
||||
body_json.room = internal_room_jid_match_rewrite(room.jid);
|
||||
body_json.action = 'promotion-request';
|
||||
body_json.nick = nick;
|
||||
body_json.from = from_jid;
|
||||
|
||||
if time and time > 0 then
|
||||
-- raise hand
|
||||
body_json.on = true;
|
||||
else
|
||||
-- lower hand, we want to inform interested parties that
|
||||
-- the visitor is no longer interested in joining the main call
|
||||
body_json.on = false;
|
||||
end
|
||||
|
||||
local msg_to_send, error = json.encode(body_json);
|
||||
|
||||
if not msg_to_send then
|
||||
module:log('error', 'Error encoding msg room:%s error:%s', room.jid, error)
|
||||
return true;
|
||||
end
|
||||
|
||||
if visitors_promotion_requests[room.jid] then
|
||||
visitors_promotion_requests[room.jid][from_jid] = {
|
||||
msg = msg_to_send;
|
||||
from = from_vnode;
|
||||
};
|
||||
else
|
||||
module:log('warn', 'Received promotion request for room %s with visitors not enabled. %s',
|
||||
room.jid, msg_to_send);
|
||||
end
|
||||
|
||||
-- let's send a notification to every moderator
|
||||
for _, occupant in room:each_occupant() do
|
||||
if occupant.role == 'moderator' and not is_admin(occupant.bare_jid) then
|
||||
send_json_message(occupant.jid, msg_to_send);
|
||||
end
|
||||
end
|
||||
|
||||
return true;
|
||||
end
|
||||
end
|
||||
|
||||
module:log('warn', 'Received promotion request from %s for room %s without active visitors', from, room.jid);
|
||||
end
|
||||
|
||||
local function connect_vnode_received(room, vnode)
|
||||
module:context(muc_domain_base):fire_event('jitsi-connect-vnode', { room = room; vnode = vnode; });
|
||||
|
||||
if not visitors_promotion_map[room.jid] then
|
||||
-- visitors is enabled
|
||||
visitors_promotion_map[room.jid] = {};
|
||||
visitors_promotion_requests[room.jid] = {};
|
||||
room._connected_vnodes = cache.new(16); -- we up to 16 vnodes for this prosody
|
||||
end
|
||||
|
||||
room._connected_vnodes:set(vnode..'.meet.jitsi', 'connected');
|
||||
end
|
||||
|
||||
local function disconnect_vnode_received(room, vnode)
|
||||
module:context(muc_domain_base):fire_event('jitsi-disconnect-vnode', { room = room; vnode = vnode; });
|
||||
|
||||
room._connected_vnodes:set(vnode..'.meet.jitsi', nil);
|
||||
|
||||
if room._connected_vnodes:count() == 0 then
|
||||
visitors_promotion_map[room.jid] = nil;
|
||||
visitors_promotion_requests[room.jid] = nil;
|
||||
room._connected_vnodes = nil;
|
||||
end
|
||||
end
|
||||
|
||||
-- listens for iq request for promotion and forward it to moderators in the meeting for approval
|
||||
-- or auto-allow it if such the config is set enabling it
|
||||
local function stanza_handler(event)
|
||||
local origin, stanza = event.origin, event.stanza;
|
||||
|
||||
if stanza.name ~= 'iq' then
|
||||
return;
|
||||
end
|
||||
|
||||
if stanza.attr.type == 'result' and sent_iq_cache:get(stanza.attr.id) then
|
||||
sent_iq_cache:set(stanza.attr.id, nil);
|
||||
return true;
|
||||
end
|
||||
|
||||
if stanza.attr.type ~= 'set' and stanza.attr.type ~= 'get' then
|
||||
return; -- We do not want to reply to these, so leave.
|
||||
end
|
||||
|
||||
local visitors_iq = event.stanza:get_child('visitors', 'jitsi:visitors');
|
||||
if not visitors_iq then
|
||||
return;
|
||||
end
|
||||
|
||||
-- set stanzas are coming from s2s connection
|
||||
if stanza.attr.type == 'set' and origin.type ~= 's2sin' then
|
||||
module:log('warn', 'not from s2s session, ignore! %s', stanza);
|
||||
return true;
|
||||
end
|
||||
|
||||
local room_jid = visitors_iq.attr.room;
|
||||
local room = get_room_from_jid(room_jid_match_rewrite(room_jid));
|
||||
|
||||
if not room then
|
||||
-- this maybe as we receive the iq from jicofo after the room is already destroyed
|
||||
module:log('debug', 'No room found %s', room_jid);
|
||||
return;
|
||||
end
|
||||
|
||||
local processed;
|
||||
-- promotion request is coming from visitors and is a set and is over the s2s connection
|
||||
local request_promotion = visitors_iq:get_child('promotion-request');
|
||||
if request_promotion then
|
||||
if not (room._connected_vnodes and room._connected_vnodes:get(stanza.attr.from)) then
|
||||
module:log('warn', 'Received forged promotion-request: %s %s %s', stanza, inspect(room._connected_vnodes), room._connected_vnodes:get(stanza.attr.from));
|
||||
return true; -- stop processing
|
||||
end
|
||||
|
||||
local display_name = visitors_iq:get_child_text('nick', 'http://jabber.org/protocol/nick');
|
||||
processed = request_promotion_received(
|
||||
room,
|
||||
request_promotion.attr.jid,
|
||||
stanza.attr.from,
|
||||
display_name,
|
||||
tonumber(request_promotion.attr.time),
|
||||
request_promotion.attr.userId,
|
||||
request_promotion.attr.forcePromote
|
||||
);
|
||||
end
|
||||
|
||||
-- connect and disconnect are only received from jicofo
|
||||
if is_admin(jid.bare(stanza.attr.from)) then
|
||||
for item in visitors_iq:childtags('connect-vnode') do
|
||||
connect_vnode_received(room, item.attr.vnode);
|
||||
processed = true;
|
||||
end
|
||||
|
||||
for item in visitors_iq:childtags('disconnect-vnode') do
|
||||
disconnect_vnode_received(room, item.attr.vnode);
|
||||
processed = true;
|
||||
end
|
||||
end
|
||||
|
||||
if not processed then
|
||||
module:log('warn', 'Unknown iq received for %s: %s', module.host, stanza);
|
||||
end
|
||||
|
||||
respond_iq_result(origin, stanza);
|
||||
return processed;
|
||||
end
|
||||
|
||||
local function process_promotion_response(room, id, approved)
|
||||
-- lets reply to participant that requested promotion
|
||||
local username = new_id():lower();
|
||||
visitors_promotion_map[room.jid][username] = {
|
||||
from = visitors_promotion_requests[room.jid][id].from;
|
||||
jid = id;
|
||||
};
|
||||
|
||||
local req_from = visitors_promotion_map[room.jid][username].from;
|
||||
local req_jid = visitors_promotion_map[room.jid][username].jid;
|
||||
local focus_occupant = get_focus_occupant(room);
|
||||
local focus_jid = focus_occupant and focus_occupant.bare_jid or nil;
|
||||
|
||||
local iq_id = new_id();
|
||||
sent_iq_cache:set(iq_id, socket.gettime());
|
||||
|
||||
local node = jid.node(room.jid);
|
||||
|
||||
module:send(st.iq({
|
||||
type='set', to = req_from, from = module.host, id = iq_id })
|
||||
:tag('visitors', {
|
||||
xmlns='jitsi:visitors',
|
||||
room = jid.join(node, muc_domain_prefix..'.'..req_from),
|
||||
focusjid = focus_jid })
|
||||
:tag('promotion-response', {
|
||||
xmlns='jitsi:visitors',
|
||||
jid = req_jid,
|
||||
username = username,
|
||||
allow = approved }):up());
|
||||
end
|
||||
|
||||
-- if room metadata does not have visitors.live set to `true` and there are no occupants in the meeting
|
||||
-- it will skip calling goLive endpoint
|
||||
local function go_live(room)
|
||||
if room._jitsi_go_live_sent then
|
||||
return;
|
||||
end
|
||||
|
||||
if not (room.jitsiMetadata and room.jitsiMetadata.visitors and room.jitsiMetadata.visitors.live) then
|
||||
return;
|
||||
end
|
||||
|
||||
local has_occupant = false;
|
||||
for _, occupant in room:each_occupant() do
|
||||
if not is_admin(occupant.bare_jid) then
|
||||
has_occupant = true;
|
||||
break;
|
||||
end
|
||||
end
|
||||
|
||||
-- when there is an occupant then go live
|
||||
if not has_occupant then
|
||||
return;
|
||||
end
|
||||
|
||||
-- let's inform the queue service
|
||||
local function cb(content_, code_, response_, request_)
|
||||
local room = room;
|
||||
if code_ ~= 200 then
|
||||
module:log('warn', 'External call to visitors_queue_service/golive failed. Code %s, Content %s',
|
||||
code_, content_)
|
||||
end
|
||||
end
|
||||
|
||||
local headers = http_headers or {};
|
||||
headers['Authorization'] = token_util:generateAsapToken();
|
||||
|
||||
local ev = {
|
||||
conference = internal_room_jid_match_rewrite(room.jid)
|
||||
};
|
||||
|
||||
room._jitsi_go_live_sent = true;
|
||||
|
||||
http.request(visitors_queue_service..'/golive', {
|
||||
headers = headers,
|
||||
method = 'POST',
|
||||
body = json.encode(ev);
|
||||
}, cb);
|
||||
end
|
||||
|
||||
module:hook('iq/host', stanza_handler, 10);
|
||||
|
||||
process_host_module(muc_domain_base, function(host_module, host)
|
||||
token_util = module:require "token/util".new(host_module);
|
||||
end);
|
||||
|
||||
process_host_module(muc_domain_prefix..'.'..muc_domain_base, function(host_module, host)
|
||||
-- if visitor mode is started, then you are not allowed to join without request/response exchange of iqs -> deny access
|
||||
-- check list of allowed jids for the room
|
||||
host_module:hook('muc-occupant-pre-join', function (event)
|
||||
local room, stanza, occupant, session = event.room, event.stanza, event.occupant, event.origin;
|
||||
|
||||
if is_healthcheck_room(room.jid) or is_admin(occupant.bare_jid) then
|
||||
return;
|
||||
end
|
||||
|
||||
-- visitors were already in the room one way or another they have access
|
||||
-- skip password challenge
|
||||
local join = stanza:get_child('x', MUC_NS);
|
||||
if join and room:get_password() and
|
||||
visitors_promotion_map[room.jid] and visitors_promotion_map[room.jid][jid.node(stanza.attr.from)] then
|
||||
join:tag('password', { xmlns = MUC_NS }):text(room:get_password());
|
||||
end
|
||||
|
||||
-- we skip any checks when auto-allow is enabled
|
||||
if auto_allow_promotion
|
||||
or ignore_list:contains(jid.host(stanza.attr.from)) -- jibri or other domains to ignore
|
||||
or is_sip_jigasi(stanza)
|
||||
or is_sip_jibri_join(stanza) then
|
||||
return;
|
||||
end
|
||||
|
||||
if visitors_promotion_map[room.jid] then
|
||||
-- now let's check for jid
|
||||
if visitors_promotion_map[room.jid][jid.node(stanza.attr.from)] -- promotion was approved
|
||||
or ignore_list:contains(jid.host(stanza.attr.from)) then -- jibri or other domains to ignore
|
||||
-- allow join
|
||||
return;
|
||||
end
|
||||
module:log('error', 'Visitor needs to be allowed by a moderator %s', stanza.attr.from);
|
||||
session.send(st.error_reply(stanza, 'cancel', 'not-allowed', 'Visitor needs to be allowed by a moderator')
|
||||
:tag('promotion-not-allowed', { xmlns = 'jitsi:visitors' }));
|
||||
return true;
|
||||
elseif is_vpaas(room) then
|
||||
-- special case for vpaas where if someone with a visitor token tries to join a room, where
|
||||
-- there are no visitors yet, we deny access
|
||||
if session.jitsi_meet_context_user and session.jitsi_meet_context_user.role == 'visitor' then
|
||||
session.log('warn', 'Deny user join as visitor in the main meeting, not approved');
|
||||
session.send(st.error_reply(
|
||||
stanza, 'cancel', 'not-allowed', 'Visitor tried to join the main room without approval')
|
||||
:tag('no-main-participants', { xmlns = 'jitsi:visitors' }));
|
||||
return true;
|
||||
end
|
||||
end
|
||||
|
||||
end, 7); -- after muc_meeting_id, the logic for not joining before jicofo
|
||||
host_module:hook('muc-room-destroyed', function (event)
|
||||
visitors_promotion_map[event.room.jid] = nil;
|
||||
visitors_promotion_requests[event.room.jid] = nil;
|
||||
end);
|
||||
|
||||
host_module:hook('muc-occupant-joined', function (event)
|
||||
local room, occupant = event.room, event.occupant;
|
||||
|
||||
if is_healthcheck_room(room.jid) or is_admin(occupant.bare_jid) or occupant.role ~= 'moderator' -- luacheck: ignore
|
||||
or not visitors_promotion_requests[event.room.jid] then
|
||||
return;
|
||||
end
|
||||
|
||||
for _,value in pairs(visitors_promotion_requests[event.room.jid]) do
|
||||
send_json_message(occupant.jid, value.msg);
|
||||
end
|
||||
end);
|
||||
host_module:hook('muc-set-affiliation', function (event)
|
||||
-- the actor can be nil if is coming from allowners or similar module we want to skip it here
|
||||
-- as we will handle it in occupant_joined
|
||||
local actor, affiliation, jid, room = event.actor, event.affiliation, event.jid, event.room;
|
||||
|
||||
if is_admin(jid) or is_healthcheck_room(room.jid) or not actor or not affiliation == 'owner' -- luacheck: ignore
|
||||
or not visitors_promotion_requests[event.room.jid] then
|
||||
return;
|
||||
end
|
||||
|
||||
-- event.jid is the bare jid of participant
|
||||
for _, occupant in room:each_occupant() do
|
||||
if occupant.bare_jid == event.jid then
|
||||
for _,value in pairs(visitors_promotion_requests[event.room.jid]) do
|
||||
send_json_message(occupant.jid, value.msg);
|
||||
end
|
||||
end
|
||||
end
|
||||
end);
|
||||
host_module:hook("message/bare", function(event)
|
||||
local stanza = event.stanza;
|
||||
|
||||
if stanza.attr.type ~= "groupchat" then
|
||||
return;
|
||||
end
|
||||
local json_data = stanza:get_child_text("json-message", "http://jitsi.org/jitmeet");
|
||||
if json_data == nil then
|
||||
return;
|
||||
end
|
||||
local data, error = json.decode(json_data);
|
||||
if not data or data.type ~= 'visitors'
|
||||
or (data.action ~= "promotion-response" and data.action ~= "demote-request") then
|
||||
if error then
|
||||
module:log('error', 'Error decoding error:%s', error);
|
||||
end
|
||||
return;
|
||||
end
|
||||
|
||||
local room = get_room_from_jid(event.stanza.attr.to);
|
||||
|
||||
local occupant_jid = event.stanza.attr.from;
|
||||
local occupant = room:get_occupant_by_real_jid(occupant_jid);
|
||||
if not occupant then
|
||||
module:log("error", "Occupant %s was not found in room %s", occupant_jid, room.jid)
|
||||
return
|
||||
end
|
||||
if occupant.role ~= 'moderator' then
|
||||
module:log('error', 'Occupant %s sending response message but not moderator in room %s',
|
||||
occupant_jid, room.jid);
|
||||
return false;
|
||||
end
|
||||
|
||||
if data.action == "demote-request" then
|
||||
if occupant.nick ~= room.jid..'/'..data.actor then
|
||||
module:log('error', 'Bad actor in demote request %s', stanza);
|
||||
event.origin.send(st.error_reply(stanza, "cancel", "bad-request"));
|
||||
return true;
|
||||
end
|
||||
|
||||
-- when demoting we want to send message to the demoted participant and to moderators
|
||||
local target_jid = room.jid..'/'..data.id;
|
||||
stanza.attr.type = 'chat'; -- it is safe as we are not using this stanza instance anymore
|
||||
stanza.attr.from = module.host;
|
||||
|
||||
for _, room_occupant in room:each_occupant() do
|
||||
-- do not send it to jicofo or back to the sender
|
||||
if room_occupant.jid ~= occupant.jid and not is_admin(room_occupant.bare_jid) then
|
||||
if room_occupant.role == 'moderator'
|
||||
or room_occupant.nick == target_jid then
|
||||
stanza.attr.to = room_occupant.jid;
|
||||
room:route_stanza(stanza);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
else
|
||||
if data.id then
|
||||
process_promotion_response(room, data.id, data.approved and 'true' or 'false');
|
||||
else
|
||||
-- we are in the case with admit all, we need to read data.ids
|
||||
for _,value in pairs(data.ids) do
|
||||
process_promotion_response(room, value, data.approved and 'true' or 'false');
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return true; -- halt processing, but return true that we handled it
|
||||
end);
|
||||
if visitors_queue_service then
|
||||
host_module:hook('muc-room-created', function (event)
|
||||
local room = event.room;
|
||||
|
||||
if is_healthcheck_room(room.jid) then
|
||||
return;
|
||||
end
|
||||
|
||||
go_live(room);
|
||||
end, -2); -- metadata hook on -1
|
||||
host_module:hook('jitsi-metadata-updated', function (event)
|
||||
if event.key == 'visitors' then
|
||||
go_live(event.room);
|
||||
end
|
||||
end);
|
||||
-- when metadata changed internally from another module
|
||||
host_module:hook('room-metadata-changed', function (event)
|
||||
go_live(event.room);
|
||||
end);
|
||||
host_module:hook('muc-occupant-joined', function (event)
|
||||
go_live(event.room);
|
||||
end);
|
||||
end
|
||||
|
||||
if always_visitors_enabled then
|
||||
local visitorsEnabledField = {
|
||||
name = "muc#roominfo_visitorsEnabled";
|
||||
type = "boolean";
|
||||
label = "Whether visitors are enabled.";
|
||||
value = 1;
|
||||
};
|
||||
-- Append "visitors enabled" to the MUC config form.
|
||||
host_module:context(host):hook("muc-disco#info", function(event)
|
||||
table.insert(event.form, visitorsEnabledField);
|
||||
end);
|
||||
host_module:context(host):hook("muc-config-form", function(event)
|
||||
table.insert(event.form, visitorsEnabledField);
|
||||
end);
|
||||
end
|
||||
end);
|
||||
|
||||
prosody.events.add_handler('pre-jitsi-authentication', function(session)
|
||||
if not session.customusername or not session.jitsi_web_query_room then
|
||||
return nil;
|
||||
end
|
||||
|
||||
local room = get_room_by_name_and_subdomain(session.jitsi_web_query_room, session.jitsi_web_query_prefix);
|
||||
if not room then
|
||||
return nil;
|
||||
end
|
||||
|
||||
if visitors_promotion_map[room.jid] and visitors_promotion_map[room.jid][session.customusername] then
|
||||
-- user was previously allowed to join, let him use the requested jid
|
||||
return session.customusername;
|
||||
end
|
||||
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,22 @@
|
||||
--- muc.lib.lua 2016-10-26 18:26:53.432377291 +0000
|
||||
+++ muc.lib.lua 2016-10-26 18:41:40.754426072 +0000
|
||||
@@ -1582,16 +1582,16 @@
|
||||
if event.allowed ~= nil then
|
||||
return event.allowed, event.error, event.condition;
|
||||
end
|
||||
+ local occupant_affiliation = self:get_affiliation(occupant.bare_jid);
|
||||
|
||||
-- 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
|
||||
+ local actor_affiliation = self:get_affiliation(actor);
|
||||
+ 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
|
21
roles/jitsi/files/prosody/modules/muc_owner_allow_kick.patch
Normal file
21
roles/jitsi/files/prosody/modules/muc_owner_allow_kick.patch
Normal file
@ -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
|
397
roles/jitsi/files/prosody/modules/poltergeist.lib.lua
Normal file
397
roles/jitsi/files/prosody/modules/poltergeist.lib.lua
Normal file
@ -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 existence 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 functions
|
||||
-- assist 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
|
||||
}
|
11
roles/jitsi/files/prosody/modules/s2sout_override1.patch
Normal file
11
roles/jitsi/files/prosody/modules/s2sout_override1.patch
Normal file
@ -0,0 +1,11 @@
|
||||
diff -r 214a679823e8 core/features.lua
|
||||
--- a/core/features.lua Mon May 01 15:10:32 2023 +0200
|
||||
+++ b/core/features.lua Wed May 24 11:53:34 2023 -0500
|
||||
@@ -4,5 +4,7 @@
|
||||
available = set.new{
|
||||
-- mod_bookmarks bundled
|
||||
"mod_bookmarks";
|
||||
+
|
||||
+ "s2sout-pre-connect-event";
|
||||
};
|
||||
};
|
14
roles/jitsi/files/prosody/modules/s2sout_override2.patch
Normal file
14
roles/jitsi/files/prosody/modules/s2sout_override2.patch
Normal file
@ -0,0 +1,14 @@
|
||||
diff -r 214a679823e8 plugins/mod_s2s.lua
|
||||
--- a/mod_s2s.lua Mon May 01 15:10:32 2023 +0200
|
||||
+++ b/mod_s2s.lua Wed May 24 11:53:34 2023 -0500
|
||||
@@ -230,6 +230,10 @@
|
||||
resolver;
|
||||
});
|
||||
end
|
||||
+
|
||||
+ local pre_event = { session = host_session; resolver = resolver };
|
||||
+ module:context(from_host):fire_event("s2sout-pre-connect", pre_event);
|
||||
+ resolver = pre_event.resolver;
|
||||
connect(resolver, listener, nil, { session = host_session });
|
||||
m_initiated_connections:with_labels(from_host):add(1)
|
||||
return true;
|
14
roles/jitsi/files/prosody/modules/stanza_router_no-log.patch
Normal file
14
roles/jitsi/files/prosody/modules/stanza_router_no-log.patch
Normal file
@ -0,0 +1,14 @@
|
||||
diff -r 423f240d1173 core/stanza_router.lua
|
||||
--- a/core/stanza_router.lua Tue Feb 21 10:06:54 2023 +0000
|
||||
+++ b/core/stanza_router.lua Wed May 24 11:56:02 2023 -0500
|
||||
@@ -207,7 +207,9 @@
|
||||
else
|
||||
local host_session = hosts[from_host];
|
||||
if not host_session then
|
||||
- log("error", "No hosts[from_host] (please report): %s", stanza);
|
||||
+ -- moved it to debug as it fills visitor's prosody logs and this is a situation where we try to send
|
||||
+ -- presence back to the main server and we don't need anyway as it came from there
|
||||
+ log("debug", "No hosts[from_host] (please report): %s", stanza);
|
||||
else
|
||||
local xmlns = stanza.attr.xmlns;
|
||||
stanza.attr.xmlns = nil;
|
539
roles/jitsi/files/prosody/modules/token/util.lib.lua
Normal file
539
roles/jitsi/files/prosody/modules/token/util.lib.lua
Normal file
@ -0,0 +1,539 @@
|
||||
-- Token authentication
|
||||
-- Copyright (C) 2021-present 8x8, Inc.
|
||||
|
||||
local basexx = require "basexx";
|
||||
local have_async, async = pcall(require, "util.async");
|
||||
local hex = require "util.hex";
|
||||
local jwt = module: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 main_util = module:require "util";
|
||||
local ends_with = main_util.ends_with;
|
||||
local http_get_with_retry = main_util.http_get_with_retry;
|
||||
local extract_subdomain = main_util.extract_subdomain;
|
||||
local starts_with = main_util.starts_with;
|
||||
local table_shallow_copy = main_util.table_shallow_copy;
|
||||
local cjson_safe = require 'cjson.safe'
|
||||
local timer = require "util.timer";
|
||||
local async = require "util.async";
|
||||
local inspect = require 'inspect';
|
||||
|
||||
local nr_retries = 3;
|
||||
local ssl = require "ssl";
|
||||
|
||||
-- TODO: Figure out a less arbitrary default cache size.
|
||||
local cacheSize = module:get_option_number("jwt_pubkey_cache_size", 128);
|
||||
|
||||
-- the cache for generated asap jwt tokens
|
||||
local jwtKeyCache = require 'util.cache'.new(cacheSize);
|
||||
|
||||
local ASAPTTL_THRESHOLD = module:get_option_number('asap_ttl_threshold', 600);
|
||||
local ASAPTTL = module:get_option_number('asap_ttl', 3600);
|
||||
local ASAPIssuer = module:get_option_string('asap_issuer', 'jitsi');
|
||||
local ASAPAudience = module:get_option_string('asap_audience', 'jitsi');
|
||||
local ASAPKeyId = module:get_option_string('asap_key_id', 'jitsi');
|
||||
local ASAPKeyPath = module:get_option_string('asap_key_path', '/etc/prosody/certs/asap.key');
|
||||
|
||||
local ASAPKey;
|
||||
local f = io.open(ASAPKeyPath, 'r');
|
||||
|
||||
if f then
|
||||
ASAPKey = f:read('*all');
|
||||
f:close();
|
||||
end
|
||||
|
||||
local Util = {}
|
||||
Util.__index = Util
|
||||
|
||||
--- Constructs util class for token verifications.
|
||||
-- Constructor that uses the passed module to extract all the
|
||||
-- needed configurations.
|
||||
-- If configuration 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");
|
||||
-- A URL that will return json file with a mapping between kids and public keys
|
||||
-- If the response Cache-Control header we will respect it and refresh it
|
||||
self.cacheKeysUrl = module:get_option_string("cache_keys_url");
|
||||
self.signatureAlgorithm = module:get_option_string("signature_algorithm");
|
||||
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 subtract 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 enabled
|
||||
-- when disabled checking domain name and tenant if available will be skipped, we will check only room name.
|
||||
self.enableDomainVerification = module:get_option_boolean('enable_domain_verification', true);
|
||||
|
||||
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
|
||||
|
||||
-- Set defaults for signature algorithm
|
||||
if self.signatureAlgorithm == nil then
|
||||
if self.asapKeyServer ~= nil then
|
||||
self.signatureAlgorithm = "RS256"
|
||||
elseif self.appSecret ~= nil then
|
||||
self.signatureAlgorithm = "HS256"
|
||||
end
|
||||
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
|
||||
|
||||
if self.cacheKeysUrl then
|
||||
self.cachedKeys = {};
|
||||
local update_keys_cache;
|
||||
update_keys_cache = async.runner(function (name)
|
||||
local content, code, cache_for;
|
||||
content, code, cache_for = http_get_with_retry(self.cacheKeysUrl, nr_retries);
|
||||
if content ~= nil then
|
||||
local keys_to_delete = table_shallow_copy(self.cachedKeys);
|
||||
-- Let's convert any certificate to public key
|
||||
for k, v in pairs(cjson_safe.decode(content)) do
|
||||
if starts_with(v, '-----BEGIN CERTIFICATE-----') then
|
||||
self.cachedKeys[k] = ssl.loadcertificate(v):pubkey();
|
||||
-- do not clean this key if it already exists
|
||||
keys_to_delete[k] = nil;
|
||||
end
|
||||
end
|
||||
-- let's schedule the clean in an hour and a half, current tokens will be valid for an hour
|
||||
timer.add_task(90*60, function ()
|
||||
for k, _ in pairs(keys_to_delete) do
|
||||
self.cachedKeys[k] = nil;
|
||||
end
|
||||
end);
|
||||
|
||||
if cache_for then
|
||||
cache_for = tonumber(cache_for);
|
||||
-- let's schedule new update 60 seconds before the cache expiring
|
||||
if cache_for > 60 then
|
||||
cache_for = cache_for - 60;
|
||||
end
|
||||
timer.add_task(cache_for, function ()
|
||||
update_keys_cache:run("update_keys_cache");
|
||||
end);
|
||||
else
|
||||
-- no cache header let's consider updating in 6hours
|
||||
timer.add_task(6*60*60, function ()
|
||||
update_keys_cache:run("update_keys_cache");
|
||||
end);
|
||||
end
|
||||
else
|
||||
module:log('warn', 'Failed to retrieve cached public keys code:%s', code);
|
||||
-- failed let's retry in 30 seconds
|
||||
timer.add_task(30, function ()
|
||||
update_keys_cache:run("update_keys_cache");
|
||||
end);
|
||||
end
|
||||
end);
|
||||
update_keys_cache:run("update_keys_cache");
|
||||
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);
|
||||
local code;
|
||||
if content == nil then
|
||||
-- If the key is not found in the cache.
|
||||
-- module:log("debug", "Cache miss for key: %s", keyId);
|
||||
local keyurl = path.join(self.asapKeyServer, hex.to(sha256(keyId))..'.pem');
|
||||
-- module:log("debug", "Fetching public key from: %s", keyurl);
|
||||
content, code = http_get_with_retry(keyurl, nr_retries);
|
||||
if content ~= nil then
|
||||
self.cache:set(keyId, content);
|
||||
else
|
||||
if code == nil then
|
||||
-- this is timout after nr_retries retries
|
||||
module:log('warn', 'Timeout retrieving %s from %s', keyId, keyurl);
|
||||
end
|
||||
end
|
||||
return content;
|
||||
else
|
||||
-- If the key is in the cache, use it.
|
||||
-- module:log("debug", "Cache hit for key: %s", keyId);
|
||||
return content;
|
||||
end
|
||||
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_room - the room 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
|
||||
-- @return false and error
|
||||
function Util:process_and_verify_token(session)
|
||||
if session.auth_token == nil then
|
||||
if self.allowEmptyToken then
|
||||
return true;
|
||||
else
|
||||
return false, "not-allowed", "token required";
|
||||
end
|
||||
end
|
||||
|
||||
local key;
|
||||
if session.public_key then
|
||||
-- We're using an public key stored in the session
|
||||
-- module:log("debug","Public key was found on the session");
|
||||
key = session.public_key;
|
||||
elseif self.asapKeyServer and session.auth_token ~= nil then
|
||||
-- We're fetching an public key from an ASAP server
|
||||
local dotFirst = session.auth_token:find("%.");
|
||||
if not dotFirst then return false, "not-allowed", "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
|
||||
local alg = header["alg"];
|
||||
if alg == nil then
|
||||
return false, "not-allowed", "'alg' claim is missing";
|
||||
end
|
||||
if alg.sub(alg,1,2) ~= "RS" then
|
||||
return false, "not-allowed", "'kid' claim only support with RS family";
|
||||
end
|
||||
|
||||
if self.cachedKeys and self.cachedKeys[kid] then
|
||||
key = self.cachedKeys[kid];
|
||||
else
|
||||
key = self:get_public_key(kid);
|
||||
end
|
||||
|
||||
if key == nil then
|
||||
return false, "not-allowed", "could not obtain public key";
|
||||
end
|
||||
elseif self.appSecret ~= nil then
|
||||
-- We're using a symmetric secret
|
||||
key = self.appSecret
|
||||
end
|
||||
|
||||
if key == nil then
|
||||
return false, "not-allowed", "signature verification key is missing";
|
||||
end
|
||||
|
||||
-- now verify the whole token
|
||||
local claims, msg = jwt.verify(
|
||||
session.auth_token,
|
||||
self.signatureAlgorithm,
|
||||
key,
|
||||
self.acceptedIssuers,
|
||||
self.acceptedAudiences
|
||||
)
|
||||
if claims ~= nil then
|
||||
if self.requireRoomClaim then
|
||||
local roomClaim = claims["room"];
|
||||
if roomClaim == nil then
|
||||
return false, "'room' claim is missing";
|
||||
end
|
||||
end
|
||||
|
||||
-- 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
|
||||
session.jitsi_meet_str_tenant = claims["context"]["tenant"];
|
||||
|
||||
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
|
||||
if claims["context"]["room"] ~= nil then
|
||||
session.jitsi_meet_context_room = claims["context"]["room"]
|
||||
end
|
||||
elseif claims["user_id"] then
|
||||
session.jitsi_meet_context_user = {};
|
||||
session.jitsi_meet_context_user.id = claims["user_id"];
|
||||
end
|
||||
|
||||
-- fire event that token has been verified and pass the session and the decoded token
|
||||
prosody.events.fire_event('jitsi-authentication-token-verified', {
|
||||
session = session;
|
||||
claims = claims;
|
||||
});
|
||||
|
||||
if session.contextRequired and claims["context"] == nil then
|
||||
return false, "not-allowed", 'jwt missing required context claim';
|
||||
end
|
||||
|
||||
return true;
|
||||
else
|
||||
return false, "not-allowed", msg;
|
||||
end
|
||||
end
|
||||
|
||||
--- Verifies room name and domain if necessary.
|
||||
-- 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 auth_room then
|
||||
if type(auth_room) == 'string' then
|
||||
auth_room = string.lower(auth_room);
|
||||
else
|
||||
module:log('warn', 'session.jitsi_meet_room not string: %s', inspect(auth_room));
|
||||
end
|
||||
end
|
||||
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 ~= auth_room and not ends_with(room, ']'..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 = extract_subdomain(room_node);
|
||||
|
||||
-- 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 from the token
|
||||
if session.jitsi_meet_context_room and (session.jitsi_meet_context_room["regex"] == true or session.jitsi_meet_context_room["regex"] == "true") then
|
||||
if target_room ~= nil then
|
||||
-- room with subdomain
|
||||
room_to_check = target_room:match(auth_room);
|
||||
else
|
||||
room_to_check = room_node:match(auth_room);
|
||||
end
|
||||
else
|
||||
-- not a regex
|
||||
room_to_check = auth_room;
|
||||
end
|
||||
-- module:log("debug", "room to check: %s", room_to_check)
|
||||
if not room_to_check then
|
||||
if not self.requireRoomClaim then
|
||||
-- if we do not require to have the room claim, and it is missing
|
||||
-- there is no point of continue and verifying the roomName and the tenant
|
||||
return true;
|
||||
end
|
||||
|
||||
return false;
|
||||
end
|
||||
end
|
||||
|
||||
if session.jitsi_meet_str_tenant
|
||||
and string.lower(session.jitsi_meet_str_tenant) ~= session.jitsi_web_query_prefix then
|
||||
module:log('warn', 'Tenant differs for user:%s group:%s url_tenant:%s token_tenant:%s',
|
||||
session.jitsi_meet_context_user and session.jitsi_meet_context_user.id or '',
|
||||
session.jitsi_meet_context_group,
|
||||
session.jitsi_web_query_prefix, session.jitsi_meet_str_tenant);
|
||||
session.jitsi_meet_tenant_mismatch = true;
|
||||
end
|
||||
|
||||
local auth_domain = string.lower(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(
|
||||
"["..subdomain_to_check.."]"..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(room_to_check, subdomain_to_check);
|
||||
end
|
||||
end
|
||||
|
||||
function Util:generateAsapToken(audience)
|
||||
if not ASAPKey then
|
||||
module:log('warn', 'No ASAP Key read, asap key generation is disabled');
|
||||
return ''
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
return Util;
|
564
roles/jitsi/files/prosody/modules/util.lib.lua
Normal file
564
roles/jitsi/files/prosody/modules/util.lib.lua
Normal file
@ -0,0 +1,564 @@
|
||||
local jid = require "util.jid";
|
||||
local timer = require "util.timer";
|
||||
local http = require "net.http";
|
||||
local cache = require "util.cache";
|
||||
|
||||
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 'conference.foo.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 component
|
||||
local roomless_iqs = {};
|
||||
|
||||
local OUTBOUND_SIP_JIBRI_PREFIXES = { 'outbound-sip-jibri@', 'sipjibriouta@', 'sipjibrioutb@' };
|
||||
local INBOUND_SIP_JIBRI_PREFIXES = { 'inbound-sip-jibri@', 'sipjibriina@', 'sipjibriina@' };
|
||||
|
||||
local split_subdomain_cache = cache.new(1000);
|
||||
local extract_subdomain_cache = cache.new(1000);
|
||||
local internal_room_jid_cache = cache.new(1000);
|
||||
|
||||
local moderated_subdomains = module:get_option_set("allowners_moderated_subdomains", {})
|
||||
local moderated_rooms = module:get_option_set("allowners_moderated_rooms", {})
|
||||
|
||||
-- Utility function to split room JID to include room name and subdomain
|
||||
-- (e.g. from room1@conference.foo.example.com/res returns (room1, example.com, res, foo))
|
||||
local function room_jid_split_subdomain(room_jid)
|
||||
local ret = split_subdomain_cache:get(room_jid);
|
||||
if ret then
|
||||
return ret.node, ret.host, ret.resource, ret.subdomain;
|
||||
end
|
||||
|
||||
local node, host, resource = jid.split(room_jid);
|
||||
|
||||
local target_subdomain = host and host:match(target_subdomain_pattern);
|
||||
local cache_value = {node=node, host=host, resource=resource, subdomain=target_subdomain};
|
||||
split_subdomain_cache:set(room_jid, cache_value);
|
||||
return node, host, resource, target_subdomain;
|
||||
end
|
||||
|
||||
--- Utility function to check and convert a room JID from
|
||||
--- virtual room1@conference.foo.example.com to real [foo]room1@conference.example.com
|
||||
-- @param room_jid the room jid to match and rewrite if needed
|
||||
-- @param stanza the stanza
|
||||
-- @return returns room jid [foo]room1@conference.example.com when it has subdomain
|
||||
-- otherwise room1@conference.example.com(the room_jid value untouched)
|
||||
local function room_jid_match_rewrite(room_jid, stanza)
|
||||
local node, _, 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;
|
||||
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 and stanza.attr and stanza.attr.id) then
|
||||
roomless_iqs[stanza.attr.id] = stanza.attr.to;
|
||||
end
|
||||
end
|
||||
|
||||
return jid.join(new_node, new_host, new_resource);
|
||||
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 internal_room_jid_match_rewrite(room_jid, stanza)
|
||||
-- first check for roomless_iqs
|
||||
if (stanza and 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
|
||||
|
||||
local ret = internal_room_jid_cache:get(room_jid);
|
||||
if ret then
|
||||
return ret;
|
||||
end
|
||||
|
||||
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);
|
||||
internal_room_jid_cache:set(room_jid, room_jid);
|
||||
return room_jid;
|
||||
end
|
||||
|
||||
local target_subdomain, target_node = extract_subdomain(node);
|
||||
if not (target_node and target_subdomain) then
|
||||
-- module:log("debug", "Not rewriting... unexpected node format: %s", node);
|
||||
internal_room_jid_cache:set(room_jid, room_jid);
|
||||
return room_jid;
|
||||
end
|
||||
|
||||
-- Ok, rewrite room_jid address to pretty format
|
||||
ret = jid.join(target_node, muc_domain_prefix..".".. target_subdomain.."."..muc_domain_base, resource);
|
||||
internal_room_jid_cache:set(room_jid, ret);
|
||||
return ret;
|
||||
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
|
||||
|
||||
-- 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_by_name_and_subdomain(room_name, subdomain)
|
||||
local room_address;
|
||||
|
||||
-- if there is a subdomain we are in multidomain mode and that subdomain is not our main host
|
||||
if subdomain and subdomain ~= "" and subdomain ~= muc_domain_base then
|
||||
room_address = jid.join("["..subdomain.."]"..room_name, muc_domain);
|
||||
else
|
||||
room_address = jid.join(room_name, muc_domain);
|
||||
end
|
||||
|
||||
return get_room_from_jid(room_address);
|
||||
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
|
||||
)
|
||||
|
||||
stanza:tag("identity"):tag("user");
|
||||
for k, v in pairs(user) do
|
||||
v = tostring(v)
|
||||
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
|
||||
end
|
||||
|
||||
stanza:up(); -- Close identity tag
|
||||
end
|
||||
|
||||
-- Utility function to check whether feature is present and enabled. Allow
|
||||
-- a feature if there are features present in the session(coming from
|
||||
-- the token) and the value of the feature is true.
|
||||
-- If features is not present in the token we skip feature detection and allow
|
||||
-- everything.
|
||||
function is_feature_allowed(features, ft)
|
||||
if (features == nil or features[ft] == "true" or features[ft] == true) then
|
||||
return true;
|
||||
else
|
||||
return false;
|
||||
end
|
||||
end
|
||||
|
||||
--- Extracts the subdomain and room name from internal jid node [foo]room1
|
||||
-- @return subdomain(optional, if extracted or nil), the room name
|
||||
function extract_subdomain(room_node)
|
||||
local ret = extract_subdomain_cache:get(room_node);
|
||||
if ret then
|
||||
return ret.subdomain, ret.room;
|
||||
end
|
||||
|
||||
local subdomain, room_name = room_node:match("^%[([^%]]+)%](.+)$");
|
||||
local cache_value = {subdomain=subdomain, room=room_name};
|
||||
extract_subdomain_cache:set(room_node, cache_value);
|
||||
return subdomain, room_name;
|
||||
end
|
||||
|
||||
function starts_with(str, start)
|
||||
if not str then
|
||||
return false;
|
||||
end
|
||||
return str:sub(1, #start) == start
|
||||
end
|
||||
|
||||
function starts_with_one_of(str, prefixes)
|
||||
if not str then
|
||||
return false;
|
||||
end
|
||||
for i=1,#prefixes do
|
||||
if starts_with(str, prefixes[i]) then
|
||||
return prefixes[i];
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
|
||||
function ends_with(str, ending)
|
||||
return ending == "" or str:sub(-#ending) == ending
|
||||
end
|
||||
|
||||
-- healthcheck rooms in jicofo starts with a string '__jicofo-health-check'
|
||||
function is_healthcheck_room(room_jid)
|
||||
return starts_with(room_jid, "__jicofo-health-check");
|
||||
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
|
||||
-- @param auth_token value to be passed as auth Bearer
|
||||
-- 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, auth_token)
|
||||
local content, code, cache_for;
|
||||
local timeout_occurred;
|
||||
local wait, done = async.waiter();
|
||||
local request_headers = http_headers or {}
|
||||
if auth_token ~= nil then
|
||||
request_headers['Authorization'] = 'Bearer ' .. auth_token
|
||||
end
|
||||
|
||||
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_;
|
||||
|
||||
-- if there is cache-control header, let's return the max-age value
|
||||
if response_ and response_.headers and response_.headers['cache-control'] then
|
||||
local vals = {};
|
||||
for k, v in response_.headers['cache-control']:gmatch('(%w+)=(%w+)') do
|
||||
vals[k] = v;
|
||||
end
|
||||
-- max-age=123 will be parsed by the regex ^ to age=123
|
||||
cache_for = vals.age;
|
||||
end
|
||||
else
|
||||
module:log("warn", "Error on GET 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 = request_headers,
|
||||
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, code, cache_for;
|
||||
end
|
||||
|
||||
-- Checks whether there is status in the <x node
|
||||
-- @param muc_x the <x element from presence
|
||||
-- @param status checks for this status
|
||||
-- @returns true if the status is found, false otherwise or if no muc_x is provided.
|
||||
function presence_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
|
||||
|
||||
-- Retrieves the focus from the room and cache it in the room object
|
||||
-- @param room The room name for which to find the occupant
|
||||
local function get_focus_occupant(room)
|
||||
return room:get_occupant_by_nick(room.jid..'/focus');
|
||||
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)
|
||||
function is_moderated(room_jid)
|
||||
if moderated_subdomains:empty() and moderated_rooms:empty() then
|
||||
return false;
|
||||
end
|
||||
|
||||
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 = extract_subdomain(room_node);
|
||||
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
|
||||
|
||||
-- check if the room tenant starts with vpaas-magic-cookie-
|
||||
-- @param room the room to check
|
||||
function is_vpaas(room)
|
||||
if not room then
|
||||
return false;
|
||||
end
|
||||
|
||||
-- stored check in room object if it exist
|
||||
if room.is_vpaas ~= nil then
|
||||
return room.is_vpaas;
|
||||
end
|
||||
|
||||
room.is_vpaas = false;
|
||||
|
||||
local node, host = jid.split(room.jid);
|
||||
if host ~= muc_domain or not node then
|
||||
return false;
|
||||
end
|
||||
local tenant, conference_name = node:match('^%[([^%]]+)%](.+)$');
|
||||
if not (tenant and conference_name) then
|
||||
return false;
|
||||
end
|
||||
|
||||
if not starts_with(tenant, 'vpaas-magic-cookie-') then
|
||||
return false;
|
||||
end
|
||||
|
||||
room.is_vpaas = true;
|
||||
return true;
|
||||
end
|
||||
|
||||
-- Returns the initiator extension if the stanza is coming from a sip jigasi
|
||||
function is_sip_jigasi(stanza)
|
||||
return stanza:get_child('initiator', 'http://jitsi.org/protocol/jigasi');
|
||||
end
|
||||
|
||||
function get_sip_jibri_email_prefix(email)
|
||||
if not email then
|
||||
return nil;
|
||||
elseif starts_with_one_of(email, INBOUND_SIP_JIBRI_PREFIXES) then
|
||||
return starts_with_one_of(email, INBOUND_SIP_JIBRI_PREFIXES);
|
||||
elseif starts_with_one_of(email, OUTBOUND_SIP_JIBRI_PREFIXES) then
|
||||
return starts_with_one_of(email, OUTBOUND_SIP_JIBRI_PREFIXES);
|
||||
else
|
||||
return nil;
|
||||
end
|
||||
end
|
||||
|
||||
function is_sip_jibri_join(stanza)
|
||||
if not stanza then
|
||||
return false;
|
||||
end
|
||||
|
||||
local features = stanza:get_child('features');
|
||||
local email = stanza:get_child_text('email');
|
||||
|
||||
if not features or not email then
|
||||
return false;
|
||||
end
|
||||
|
||||
for i = 1, #features do
|
||||
local feature = features[i];
|
||||
if feature.attr and feature.attr.var and feature.attr.var == "http://jitsi.org/protocol/jibri" then
|
||||
if get_sip_jibri_email_prefix(email) then
|
||||
module:log("debug", "Occupant with email %s is a sip jibri ", email);
|
||||
return true;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
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('info', '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
|
||||
|
||||
function table_shallow_copy(t)
|
||||
local t2 = {}
|
||||
for k, v in pairs(t) do
|
||||
t2[k] = v
|
||||
end
|
||||
return t2
|
||||
end
|
||||
|
||||
return {
|
||||
OUTBOUND_SIP_JIBRI_PREFIXES = OUTBOUND_SIP_JIBRI_PREFIXES;
|
||||
INBOUND_SIP_JIBRI_PREFIXES = INBOUND_SIP_JIBRI_PREFIXES;
|
||||
extract_subdomain = extract_subdomain;
|
||||
is_feature_allowed = is_feature_allowed;
|
||||
is_healthcheck_room = is_healthcheck_room;
|
||||
is_moderated = is_moderated;
|
||||
is_sip_jibri_join = is_sip_jibri_join;
|
||||
is_sip_jigasi = is_sip_jigasi;
|
||||
is_vpaas = is_vpaas;
|
||||
get_focus_occupant = get_focus_occupant;
|
||||
get_room_from_jid = get_room_from_jid;
|
||||
get_room_by_name_and_subdomain = get_room_by_name_and_subdomain;
|
||||
get_sip_jibri_email_prefix = get_sip_jibri_email_prefix;
|
||||
async_handler_wrapper = async_handler_wrapper;
|
||||
presence_check_status = presence_check_status;
|
||||
process_host_module = process_host_module;
|
||||
room_jid_match_rewrite = room_jid_match_rewrite;
|
||||
room_jid_split_subdomain = room_jid_split_subdomain;
|
||||
internal_room_jid_match_rewrite = internal_room_jid_match_rewrite;
|
||||
update_presence_identity = update_presence_identity;
|
||||
http_get_with_retry = http_get_with_retry;
|
||||
ends_with = ends_with;
|
||||
starts_with = starts_with;
|
||||
starts_with_one_of = starts_with_one_of;
|
||||
table_shallow_copy = table_shallow_copy;
|
||||
};
|
@ -11,3 +11,6 @@
|
||||
|
||||
- name: restart jitsi-confmapper
|
||||
service: name=jitsi-confmapper state={{ jitsi_jigasi | ternary('restarted', 'stopped') }}
|
||||
|
||||
- name: restart jitsi-excalidraw
|
||||
service: name=jitsi-excalidraw state=restarted
|
||||
|
@ -3,8 +3,13 @@
|
||||
- name: Remove temp files
|
||||
file: path={{ item }} state=absent
|
||||
loop:
|
||||
- "{{ jitsi_root_dir }}/tmp/jicofo-1.1-SNAPSHOT"
|
||||
- "{{ jitsi_root_dir }}/src/jicofo/target"
|
||||
- "{{ jitsi_root_dir }}/tmp/jicofo-stable-jitsi-meet_{{ jitsi_version }}.tar.gz"
|
||||
- "{{ jitsi_root_dir }}/tmp/jicofo-stable-jitsi-meet_{{ jitsi_version }}"
|
||||
- "{{ jitsi_root_dir }}/tmp/jigasi-master.tar.gz"
|
||||
- "{{ jitsi_root_dir }}/tmp/jigasi-master"
|
||||
- "{{ jitsi_root_dir }}/tmp/jigasi-linux-x64-1.1-SNAPSHOT"
|
||||
- "{{ jitsi_root_dir }}/src/jigasi/target"
|
||||
- "{{ jitsi_root_dir }}/tmp/jitsi-meet-stable-jitsi-meet_{{ jitsi_version }}.tar.gz"
|
||||
- "{{ jitsi_root_dir }}/tmp/jitsi-meet-stable-jitsi-meet_{{ jitsi_version }}"
|
||||
- "{{ jitsi_root_dir }}/tmp/excalidraw-backend-{{ jitsi_excalidraw_version }}.tar.gz"
|
||||
- "{{ jitsi_root_dir }}/tmp/excalidraw-backend-{{ jitsi_excalidraw_version }}"
|
||||
tags: jitsi
|
||||
|
@ -8,7 +8,7 @@
|
||||
|
||||
- name: Register XMPP accounts
|
||||
block:
|
||||
- name: Reload prosody
|
||||
- name: Restart prosody
|
||||
service: name=prosody state=restarted
|
||||
|
||||
- name: register XMPP users
|
||||
|
@ -12,14 +12,6 @@
|
||||
owner: "{{ jitsi_user }}"
|
||||
group: "{{ jitsi_user }}"
|
||||
mode: 700
|
||||
- dir: "{{ jitsi_root_dir }}/src/videobridge"
|
||||
owner: "{{ jitsi_user }}"
|
||||
- dir: "{{ jitsi_root_dir }}/src/jicofo"
|
||||
owner: "{{ jitsi_user }}"
|
||||
- dir: "{{ jitsi_root_dir }}/src/jigasi"
|
||||
owner: "{{ jitsi_user }}"
|
||||
- dir: "{{ jitsi_root_dir }}/src/meet"
|
||||
owner: "{{ jitsi_user }}"
|
||||
- dir: "{{ jitsi_root_dir }}/videobridge"
|
||||
- dir: "{{ jitsi_root_dir }}/jibri"
|
||||
- dir: "{{ jitsi_root_dir }}/jicofo"
|
||||
@ -51,4 +43,6 @@
|
||||
group: "{{ jitsi_user }}"
|
||||
mode: 700
|
||||
- dir: "{{ jitsi_root_dir }}/confmapper"
|
||||
- dir: "{{ jitsi_root_dir }}/prosody/modules"
|
||||
- dir: "{{ jitsi_root_dir }}/excalidraw"
|
||||
tags: jitsi
|
||||
|
@ -4,6 +4,46 @@
|
||||
set_fact: jitsi_jigasi={{ (jitsi_jigasi_sip_user is defined and jitsi_jigasi_sip_secret is defined) | ternary(True, False) }}
|
||||
tags: jitsi
|
||||
|
||||
# Detect jicofo version, if already installed
|
||||
- block:
|
||||
- import_tasks: ../includes/webapps_set_install_mode.yml
|
||||
vars:
|
||||
- root_dir: "{{ jitsi_root_dir }}"
|
||||
- version: "{{ jitsi_version }}"
|
||||
- version_file: ansible_jicofo_version
|
||||
- set_fact: jitsi_jicofo_install_mode={{ install_mode }}
|
||||
tags: jitsi
|
||||
|
||||
# Detect jigasi version, if already installed
|
||||
- block:
|
||||
- import_tasks: ../includes/webapps_set_install_mode.yml
|
||||
vars:
|
||||
- root_dir: "{{ jitsi_root_dir }}"
|
||||
- version: "{{ jitsi_version }}"
|
||||
- version_file: ansible_jigasi_version
|
||||
- set_fact: jitsi_jigasi_install_mode={{ install_mode }}
|
||||
tags: jitsi
|
||||
|
||||
# Detect meet version, if already installed
|
||||
- block:
|
||||
- import_tasks: ../includes/webapps_set_install_mode.yml
|
||||
vars:
|
||||
- root_dir: "{{ jitsi_root_dir }}"
|
||||
- version: "{{ jitsi_version }}"
|
||||
- version_file: ansible_meet_version
|
||||
- set_fact: jitsi_meet_install_mode={{ install_mode }}
|
||||
tags: jitsi
|
||||
|
||||
# Detect excalidraw version, if already installed
|
||||
- block:
|
||||
- import_tasks: ../includes/webapps_set_install_mode.yml
|
||||
vars:
|
||||
- root_dir: "{{ jitsi_root_dir }}"
|
||||
- version: "{{ jitsi_excalidraw_version }}"
|
||||
- version_file: ansible_excalidraw_version
|
||||
- set_fact: jitsi_excalidraw_install_mode={{ install_mode }}
|
||||
tags: jitsi
|
||||
|
||||
- name: Generate a random secret for videobridge
|
||||
block:
|
||||
- import_tasks: ../includes/get_rand_pass.yml
|
||||
@ -97,17 +137,3 @@
|
||||
register: jitsi_key_file
|
||||
tags: jitsi
|
||||
|
||||
- name: Check if jicofo is built
|
||||
stat: path={{ jitsi_root_dir }}/jicofo/jicofo.sh
|
||||
register: jitsi_jicofo_script
|
||||
tags: jitsi
|
||||
|
||||
- name: Check if jigasi is built
|
||||
stat: path={{ jitsi_root_dir }}/jigasi/jigasi.sh
|
||||
register: jitsi_jigasi_script
|
||||
tags: jitsi
|
||||
|
||||
- name: Check if meet is installed
|
||||
stat: path={{ jitsi_root_dir }}/meet/index.html
|
||||
register: jitsi_meet_index
|
||||
tags: jitsi
|
||||
|
@ -3,38 +3,19 @@
|
||||
- name: Install dependencies
|
||||
yum:
|
||||
name:
|
||||
- java-11-openjdk-devel
|
||||
- java-17-openjdk-devel
|
||||
- git
|
||||
- nodejs # needed to build meet
|
||||
- libXScrnSaver # needed for jigasi
|
||||
- python3 # needed for confmapper
|
||||
- make
|
||||
tags: jitsi
|
||||
|
||||
- name: Detect exact JRE version
|
||||
command: rpm -q java-11-openjdk
|
||||
changed_when: False
|
||||
register: jitsi_jre11_version
|
||||
tags: jitsi
|
||||
|
||||
- name: Select JRE 11 as default version
|
||||
alternatives:
|
||||
name: "{{ item.name }}"
|
||||
link: "{{ item.link }}"
|
||||
path: "{{ item.path }}"
|
||||
loop:
|
||||
- name: java
|
||||
link: /usr/bin/java
|
||||
path: /usr/lib/jvm/{{ jitsi_jre11_version.stdout | trim }}/bin/java
|
||||
- name: javac
|
||||
link: /usr/bin/javac
|
||||
path: /usr/lib/jvm/{{ jitsi_jre11_version.stdout | trim }}/bin/javac
|
||||
- name: jre_openjdk
|
||||
link: /usr/lib/jvm/jre-openjdk
|
||||
path: /usr/lib/jvm/{{ jitsi_jre11_version.stdout | trim }}
|
||||
- name: java_sdk_openjdk
|
||||
link: /usr/lib/jvm/java-openjdk
|
||||
path: /usr/lib/jvm/{{ jitsi_jre11_version.stdout | trim }}
|
||||
- lua-ldap # All the lua libs are for prosody
|
||||
- lua-cyrussasl
|
||||
- lua-cjson
|
||||
- lua-basexx
|
||||
- lua-luaossl
|
||||
- lua-inspect
|
||||
- libjwt
|
||||
tags: jitsi
|
||||
|
||||
# If you use an Let's Encrypt cert, it might not be there yet. In this case, create a link
|
||||
@ -50,89 +31,140 @@
|
||||
when: not jitsi_key_file.stat.exists
|
||||
tags: jitsi
|
||||
|
||||
# This file used to contain proxy settings for maven
|
||||
# now this is handled in a maven general dir, so remove it from here
|
||||
- name: Remove local maven configuration
|
||||
file: path={{ jitsi_root_dir }}/.m2/settings.xml state=absent
|
||||
- name: Install prosody modules
|
||||
synchronize:
|
||||
src: prosody/modules/
|
||||
dest: "{{ jitsi_root_dir }}/prosody/modules/"
|
||||
recursive: true
|
||||
notify: restart prosody
|
||||
tags: jitsi
|
||||
|
||||
# Now, for every component, we will clone or update the repo.
|
||||
# If the repo changed since the last run, we rebuild and restart the corresponding component
|
||||
- name: Clone jicofo repo
|
||||
git:
|
||||
repo: "{{ jitsi_jicofo_git_url }}"
|
||||
dest: "{{ jitsi_root_dir }}/src/jicofo"
|
||||
force: True
|
||||
depth: 1
|
||||
single_branch: True
|
||||
become_user: "{{ jitsi_user }}"
|
||||
register: jitsi_jicofo_git
|
||||
- name: Install bypass_pwd module for prosody
|
||||
template: src=mod_jibri_bypass_pwd.lua.j2 dest={{ jitsi_root_dir }}/prosody/modules/mod_jibri_bypass_pwd.lua
|
||||
notify: restart prosody
|
||||
tags: jitsi
|
||||
|
||||
- name: Install or update jicofo
|
||||
- when: jitsi_jicofo_install_mode != 'none'
|
||||
block:
|
||||
- name: Download Jitsi jicofo archive
|
||||
get_url:
|
||||
url: "{{ jitsi_jicofo_archive_url }}"
|
||||
dest: "{{ jitsi_root_dir }}/tmp"
|
||||
checksum: sha256:{{ jitsi_jicofo_archive_sha256 }}
|
||||
become_user: "{{ jitsi_user }}"
|
||||
|
||||
- name: Extract Jitsi Jicofo archive
|
||||
unarchive:
|
||||
src: "{{ jitsi_root_dir }}/tmp/jicofo-stable-jitsi-meet_{{ jitsi_version }}.tar.gz"
|
||||
dest: "{{ jitsi_root_dir }}/tmp/"
|
||||
remote_src: true
|
||||
become_user: "{{ jitsi_user }}"
|
||||
|
||||
- name: Build jicofo
|
||||
command: /opt/maven/apache-maven/bin/mvn package -DskipTests -Dassembly.skipAssembly=false
|
||||
args:
|
||||
chdir: "{{ jitsi_root_dir }}/src/jicofo"
|
||||
chdir: "{{ jitsi_root_dir }}/tmp/jicofo-stable-jitsi-meet_{{ jitsi_version }}"
|
||||
environment:
|
||||
JAVA_HOME: /usr/lib/jvm/java-17
|
||||
become_user: "{{ jitsi_user }}"
|
||||
|
||||
- name: Extract jicofo archive
|
||||
unarchive:
|
||||
src: "{{ jitsi_root_dir }}/src/jicofo/jicofo/target/jicofo-1.1-SNAPSHOT-archive.zip"
|
||||
dest: "{{ jitsi_root_dir }}/tmp/"
|
||||
remote_src: True
|
||||
- name: Install jicofo jar
|
||||
copy:
|
||||
src: "{{ jitsi_root_dir }}/tmp/jicofo-stable-jitsi-meet_{{ jitsi_version }}/jicofo/target/jicofo-1.1-SNAPSHOT-jar-with-dependencies.jar"
|
||||
dest: "{{ jitsi_root_dir }}/jicofo/jicofo.jar"
|
||||
remote_src: true
|
||||
|
||||
- name: Install jicofo startup script
|
||||
copy:
|
||||
src: "{{ jitsi_root_dir }}/tmp/jicofo-stable-jitsi-meet_{{ jitsi_version }}/resources/jicofo.sh"
|
||||
dest: "{{ jitsi_root_dir }}/jicofo/jicofo.sh"
|
||||
mode: 0755
|
||||
owner: root
|
||||
group: root
|
||||
remote_src: true
|
||||
|
||||
- name: Write version
|
||||
copy: content={{ jitsi_version }} dest={{ jitsi_root_dir }}/meta/ansible_jicofo_version
|
||||
|
||||
- name: Move jicofo to its final directory
|
||||
synchronize:
|
||||
src: "{{ jitsi_root_dir }}/tmp/jicofo-1.1-SNAPSHOT/"
|
||||
dest: "{{ jitsi_root_dir }}/jicofo/"
|
||||
recursive: True
|
||||
delete: True
|
||||
compress: False
|
||||
delegate_to: "{{ inventory_hostname }}"
|
||||
notify: restart jitsi-jicofo
|
||||
when: (jitsi_jicofo_git.changed and jitsi_manage_upgrade) or not jitsi_jicofo_script.stat.exists
|
||||
tags: jitsi
|
||||
|
||||
- name: Clone jigasi repo
|
||||
git:
|
||||
repo: "{{ jitsi_jigasi_git_url }}"
|
||||
dest: "{{ jitsi_root_dir }}/src/jigasi"
|
||||
force: True
|
||||
depth: 1
|
||||
single_branch: True
|
||||
become_user: "{{ jitsi_user }}"
|
||||
register: jitsi_jigasi_git
|
||||
tags: jitsi
|
||||
|
||||
- name: Install or update jigasi
|
||||
- when: jitsi_jigasi_install_mode != 'none'
|
||||
block:
|
||||
- name: Download Jitsi jigasi archive
|
||||
get_url:
|
||||
url: "{{ jitsi_jigasi_archive_url }}"
|
||||
dest: "{{ jitsi_root_dir }}/tmp"
|
||||
become_user: "{{ jitsi_user }}"
|
||||
|
||||
- name: Extract Jitsi Jigasi archive
|
||||
unarchive:
|
||||
src: "{{ jitsi_root_dir }}/tmp/jigasi-master.tar.gz"
|
||||
dest: "{{ jitsi_root_dir }}/tmp/"
|
||||
remote_src: true
|
||||
become_user: "{{ jitsi_user }}"
|
||||
|
||||
- name: Build jigasi
|
||||
command: /opt/maven/apache-maven/bin/mvn package -DskipTests -Dassembly.skipAssembly=false
|
||||
args:
|
||||
chdir: "{{ jitsi_root_dir }}/src/jigasi"
|
||||
chdir: "{{ jitsi_root_dir }}/tmp/jigasi-master"
|
||||
environment:
|
||||
JAVA_HOME: /usr/lib/jvm/java-17
|
||||
become_user: "{{ jitsi_user }}"
|
||||
|
||||
- name: Extract jigasi archive
|
||||
unarchive:
|
||||
src: "{{ jitsi_root_dir }}/src/jigasi/target/jigasi-linux-x64-1.1-SNAPSHOT.zip"
|
||||
src: "{{ jitsi_root_dir }}/tmp/jigasi-master/target/jigasi-linux-x64-1.1-SNAPSHOT.zip"
|
||||
dest: "{{ jitsi_root_dir }}/tmp/"
|
||||
remote_src: True
|
||||
|
||||
# - name: Link libunix-java lib
|
||||
# file: src=libunix-0.5.1.so dest={{ jitsi_root_dir }}/tmp/jigasi-linux-x64-1.1-SNAPSHOT/lib/libunix-java.so state=link
|
||||
remote_src: true
|
||||
|
||||
- name: Move jigasi to its final directory
|
||||
synchronize:
|
||||
src: "{{ jitsi_root_dir }}/tmp/jigasi-linux-x64-1.1-SNAPSHOT/"
|
||||
dest: "{{ jitsi_root_dir }}/jigasi/"
|
||||
recursive: True
|
||||
delete: True
|
||||
compress: False
|
||||
recursive: true
|
||||
delete: true
|
||||
compress: false
|
||||
delegate_to: "{{ inventory_hostname }}"
|
||||
notify: restart jitsi-jigasi
|
||||
when: (jitsi_jigasi_git.changed and jitsi_manage_upgrade) or not jitsi_jigasi_script.stat.exists
|
||||
|
||||
- name: Write version
|
||||
copy: content={{ jitsi_version }} dest={{ jitsi_root_dir }}/meta/ansible_jigasi_version
|
||||
|
||||
tags: jitsi
|
||||
|
||||
- when: jitsi_excalidraw_install_mode != 'none'
|
||||
block:
|
||||
|
||||
- name: Download Excalidraw backend
|
||||
get_url:
|
||||
url: "{{ jitsi_excalidraw_archive_url }}"
|
||||
dest: "{{ jitsi_root_dir }}/tmp"
|
||||
checksum: sha256:{{ jitsi_excalidraw_archive_sha256 }}
|
||||
become_user: "{{ jitsi_user }}"
|
||||
|
||||
- name: Extract Excalidraw archive
|
||||
unarchive:
|
||||
src: "{{ jitsi_root_dir }}/tmp/excalidraw-backend-{{ jitsi_excalidraw_version }}.tar.gz"
|
||||
dest: "{{ jitsi_root_dir }}/tmp/"
|
||||
remote_src: true
|
||||
become_user: "{{ jitsi_user }}"
|
||||
|
||||
- name: Install node dependencies
|
||||
npm: path={{ jitsi_root_dir }}/tmp/excalidraw-backend-{{ jitsi_excalidraw_version }}
|
||||
become_user: "{{ jitsi_user }}"
|
||||
|
||||
- name: Install Excalidraw backend
|
||||
synchronize:
|
||||
src: "{{ jitsi_root_dir }}/tmp/excalidraw-backend-{{ jitsi_excalidraw_version }}/"
|
||||
dest: "{{ jitsi_root_dir }}/excalidraw/"
|
||||
recursive: true
|
||||
delete: true
|
||||
compress: false
|
||||
delegate_to: "{{ inventory_hostname }}"
|
||||
|
||||
- name: Write installed version
|
||||
copy: content={{ jitsi_excalidraw_version }} dest={{ jitsi_root_dir }}/meta/ansible_excalidraw_version
|
||||
|
||||
tags: jitsi
|
||||
|
||||
- name: Deploy systemd unit
|
||||
@ -141,11 +173,13 @@
|
||||
- jitsi-jicofo
|
||||
- jitsi-jigasi
|
||||
- jitsi-confmapper
|
||||
- jitsi-excalidraw
|
||||
register: jitsi_units
|
||||
notify:
|
||||
- restart jitsi-jicofo
|
||||
- restart jitsi-jigasi
|
||||
- restart jitsi-confmapper
|
||||
- restart jitsi-excalidraw
|
||||
tags: jitsi
|
||||
|
||||
- name: Reload systemd
|
||||
@ -153,39 +187,35 @@
|
||||
when: jitsi_units.results | selectattr('changed', 'equalto', True) | list | length > 0
|
||||
tags: jitsi
|
||||
|
||||
- name: Clone jitsi meet
|
||||
git:
|
||||
repo: "{{ jitsi_meet_git_url }}"
|
||||
dest: "{{ jitsi_root_dir }}/src/meet"
|
||||
force: True
|
||||
depth: 1
|
||||
single_branch: True
|
||||
register: jitsi_meet_git
|
||||
become_user: "{{ jitsi_user }}"
|
||||
tags: jitsi
|
||||
|
||||
- name: Install or update jitsi meet
|
||||
- when: jitsi_meet_install_mode != 'none'
|
||||
block:
|
||||
- name: Download Jitsi Meet archive
|
||||
get_url:
|
||||
url: "{{ jitsi_meet_archive_url }}"
|
||||
dest: "{{ jitsi_root_dir }}/tmp"
|
||||
checksum: sha256:{{ jitsi_meet_archive_sha256 }}
|
||||
become_user: "{{ jitsi_user }}"
|
||||
|
||||
- name: Extract Jitsi Meet archive
|
||||
unarchive:
|
||||
src: "{{ jitsi_root_dir }}/tmp/jitsi-meet-stable-jitsi-meet_{{ jitsi_version }}.tar.gz"
|
||||
dest: "{{ jitsi_root_dir }}/tmp/"
|
||||
remote_src: true
|
||||
become_user: "{{ jitsi_user }}"
|
||||
|
||||
- name: Clear node_modules cache
|
||||
file: path={{ jitsi_root_dir }}/src/meet/node_modules state=absent
|
||||
file: path={{ jitsi_root_dir }}/tmp/jitsi-meet-stable-jitsi-meet_{{ jitsi_version }}/node_modules state=absent
|
||||
|
||||
- name: Install jitsi meet node dependencies
|
||||
npm: path={{ jitsi_root_dir }}/src/meet
|
||||
npm: path={{ jitsi_root_dir }}/tmp/jitsi-meet-stable-jitsi-meet_{{ jitsi_version }}
|
||||
become_user: "{{ jitsi_user }}"
|
||||
|
||||
- name: Build jitsi meet
|
||||
command: make
|
||||
args:
|
||||
chdir: "{{ jitsi_root_dir }}/src/meet"
|
||||
chdir: "{{ jitsi_root_dir }}/tmp/jitsi-meet-stable-jitsi-meet_{{ jitsi_version }}"
|
||||
become_user: "{{ jitsi_user }}"
|
||||
|
||||
#- name: Reset git (so next run won't detect a change)
|
||||
# command: git checkout {{ jitsi_root_dir }}/src/meet/resources/load-test/package-lock.json
|
||||
# changed_when: False
|
||||
# args:
|
||||
# chdir: "{{ jitsi_root_dir }}/src/meet"
|
||||
# become_user: "{{ jitsi_user }}"
|
||||
|
||||
- name: Deploy new jitsi meet version
|
||||
shell: |
|
||||
rm -rf {{ jitsi_root_dir }}/meet/*
|
||||
@ -193,8 +223,11 @@
|
||||
cp -r *.js *.html resources/*.txt connection_optimization favicon.ico fonts images libs static sounds LICENSE lang {{ jitsi_root_dir }}/meet/
|
||||
cp css/all.css {{ jitsi_root_dir }}/meet/css/
|
||||
args:
|
||||
chdir: "{{ jitsi_root_dir }}/src/meet"
|
||||
when: (jitsi_meet_git.changed and jitsi_manage_upgrade) or not jitsi_meet_index.stat.exists
|
||||
chdir: "{{ jitsi_root_dir }}/tmp/jitsi-meet-stable-jitsi-meet_{{ jitsi_version }}"
|
||||
|
||||
- name: Write installed version
|
||||
copy: content={{ jitsi_version }} dest={{ jitsi_root_dir }}/meta/ansible_meet_version
|
||||
|
||||
tags: jitsi
|
||||
|
||||
- name: Update languages
|
||||
@ -214,12 +247,3 @@
|
||||
notify: restart jitsi-confmapper
|
||||
tags: jitsi
|
||||
|
||||
- name: Ensure prosody module dir exists
|
||||
file: path=/opt/prosody/modules/ state=directory
|
||||
tags: jitsi
|
||||
|
||||
- name: Install bypass_pwd module for prosody
|
||||
template: src=mod_jibri_bypass_pwd.lua.j2 dest=/opt/prosody/modules/mod_jibri_bypass_pwd.lua
|
||||
notify: reload prosody
|
||||
tags: jitsi
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
---
|
||||
|
||||
- name: Start and enable services
|
||||
service: name=jitsi-jicofo state=started enabled=True
|
||||
service: name=jitsi-jicofo state=started enabled=true
|
||||
tags: jitsi
|
||||
|
||||
- name: Start and enable jigasi
|
||||
@ -11,3 +11,7 @@
|
||||
- name: Start and enable confmapper
|
||||
service: name=jitsi-confmapper state={{ jitsi_jigasi | ternary('started', 'stopped') }} enabled={{ jitsi_jigasi }}
|
||||
tags: jitsi
|
||||
|
||||
- name: Start and enable excalidraw
|
||||
service: name=jitsi-excalidraw state=started enabled=true
|
||||
tags: jitsi
|
||||
|
24
roles/jitsi/templates/jitsi-excalidraw.service.j2
Normal file
24
roles/jitsi/templates/jitsi-excalidraw.service.j2
Normal file
@ -0,0 +1,24 @@
|
||||
[Unit]
|
||||
Description=Jitsi Whiteboard backend
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User={{ jitsi_user }}
|
||||
Group={{ jitsi_user }}
|
||||
Environment=PORT=3018
|
||||
Environment=NODE_ENV=production
|
||||
WorkingDirectory={{ jitsi_root_dir }}/excalidraw
|
||||
ExecStart=/bin/npm start
|
||||
PrivateTmp=yes
|
||||
PrivateDevices=yes
|
||||
ProtectSystem=full
|
||||
ProtectHome=yes
|
||||
NoNewPrivileges=yes
|
||||
SyslogIdentifier=jitsi-excalidraw
|
||||
Restart=on-failure
|
||||
StartLimitInterval=0
|
||||
RestartSec=30
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
@ -15,6 +15,8 @@ ReadOnlyDirectories={{ jitsi_root_dir }}/etc {{ jitsi_root_dir }}/jicofo
|
||||
Restart=on-failure
|
||||
StartLimitInterval=0
|
||||
RestartSec=30
|
||||
Environment=JAVA_HOME=/usr/lib/jvm/java-17
|
||||
Environment=PATH=/usr/lib/jvm/java-17/bin:/usr/local/sbin:/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/bin
|
||||
Environment=JAVA_SYS_PROPS=-Dconfig.file={{ jitsi_root_dir }}/etc/jicofo/jicofo.conf
|
||||
ExecStart=/opt/jitsi/jicofo/jicofo.sh \
|
||||
${JICOFO_OPT}
|
||||
|
@ -15,6 +15,8 @@ ProtectSystem=full
|
||||
Restart=on-failure
|
||||
StartLimitInterval=0
|
||||
RestartSec=30
|
||||
Environment=JAVA_HOME=/usr/lib/jvm/java-17
|
||||
Environment=PATH=/usr/lib/jvm/java-17/bin:/usr/local/sbin:/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/bin
|
||||
ExecStart=/opt/jitsi/jigasi/jigasi.sh \
|
||||
--configdir={{ jitsi_root_dir }}/etc \
|
||||
--configdirname=jigasi \
|
||||
|
@ -26,6 +26,14 @@ server {
|
||||
# TODO : rate limit these endpoints to prevent room listing
|
||||
}
|
||||
|
||||
# Excalidraw
|
||||
location /socket.io {
|
||||
proxy_pass http://localhost:3018;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
}
|
||||
|
||||
# BOSH endpoint
|
||||
location /http-bind {
|
||||
proxy_socket_keepalive on;
|
||||
|
@ -1,4 +1,6 @@
|
||||
|
||||
plugin_paths = { "{{ jitsi_root_dir }}/prosody/modules" }
|
||||
|
||||
muc_mapper_domain_base = "{{ jitsi_domain }}";
|
||||
admins = { "{{ jitsi_jicofo_xmpp_user }}@{{ jitsi_auth_domain }}" };
|
||||
http_default_host = "{{ jitsi_domain }}";
|
||||
|
@ -3,10 +3,9 @@
|
||||
jitsi_root_dir: /opt/jitsi
|
||||
jitsi_jibri_user: jibri
|
||||
|
||||
jitsi_jibri_git_url: https://github.com/jitsi/jibri.git
|
||||
|
||||
# Should ansible manage upgrades or only initial install
|
||||
jitsi_jibri_manage_upgrade: "{{ jitsi_manage_upgrade | default(True) }}"
|
||||
jitsi_jibri_version: "{{ jitsi_version | default('9584') }}"
|
||||
# Jibri as no release, nor tag, so use master
|
||||
jitsi_jibri_archive_url: https://github.com/jitsi/jibri/archive/refs/heads/master.tar.gz
|
||||
|
||||
jitsi_jibri_domain: "{{ jitsi_domain | default(inventory_hostname) }}"
|
||||
jitsi_jibri_auth_domain: "{{ jitsi_auth_domain | default('auth.' ~ jitsi_domain) }}"
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user