Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# The build artifacts of the jitsi-meet project.
build/*

# Generated Allure test reports.
tests/prosody/allure-report/*
tests/prosody/allure-results/*

doc/*

# Third-party source code which we (1) do not want to modify or (2) try to
Expand Down
49 changes: 49 additions & 0 deletions .github/workflows/prosody-plugin-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
name: Prosody Plugin Tests

on:
push:
paths:
- 'resources/prosody-plugins/**'
- 'tests/prosody/**'
pull_request:
paths:
- 'resources/prosody-plugins/**'
- 'tests/prosody/**'

jobs:
test:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4

- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: tests/prosody/package-lock.json

- name: Install Lua and busted
run: |
sudo apt-get update -q
sudo apt-get install -y --no-install-recommends lua5.4 luarocks libssl-dev
sudo luarocks install busted
sudo luarocks install basexx
sudo luarocks install openssl
sudo luarocks install lua-cjson

- name: Install test dependencies
run: npm install
working-directory: tests/prosody

- name: Run tests
run: npm test
working-directory: tests/prosody

- name: Upload Allure report
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: allure-report
path: tests/prosody/allure-report
retention-days: 30
8 changes: 8 additions & 0 deletions resources/prosody-plugins/luajwtjitsi.lib.lua
Original file line number Diff line number Diff line change
Expand Up @@ -260,4 +260,12 @@ function M.verify(token, expectedAlgo, key, acceptedIssuers, acceptedAudiences)
return body
end

-- Expose internals for unit testing (harmless in production).
M._internals = {
verify_claim = verify_claim,
split_token = split_token,
parse_token = parse_token,
strip_signature = strip_signature,
}

return M
13 changes: 12 additions & 1 deletion resources/prosody-plugins/mod_muc_meeting_id.lua
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
local jid = require 'util.jid';
local json = require 'cjson.safe';
local st = require "util.stanza";
local queue = require "util.queue";
local uuid_gen = require "util.uuid".generate;
local main_util = module:require "util";
Expand Down Expand Up @@ -110,8 +111,18 @@ module:hook('muc-occupant-pre-join', function (event)
process_region(event.origin, stanza);
end

if is_health_room then
-- Only jicofo (focus) may join health-check rooms.
if not ends_with(occupant.nick, '/focus') then
module:log('info', 'Blocking non-focus participant from health-check room: %s', room.jid);
event.origin.send(st.error_reply(stanza, 'cancel', 'service-unavailable'));
return true;
end
return;
end

-- we skip processing only if jicofo_lock is set to false
if room._data.jicofo_lock == false or is_health_room then
if room._data.jicofo_lock == false then
return;
end

Expand Down
226 changes: 137 additions & 89 deletions resources/prosody-plugins/mod_muc_size.lua
Original file line number Diff line number Diff line change
@@ -1,6 +1,27 @@
-- Prosody IM
-- Copyright (C) 2021-present 8x8, Inc.
--
-- mod_muc_size exposes HTTP endpoints that let external services query the
-- occupancy and participant details of MUC rooms managed by the associated
-- conference component. It provides three routes:
--
-- GET /room-size?room=<name>&domain=<base>[&subdomain=<sub>][&token=<jwt>]
-- Returns {"participants": N} where N is the non-focus occupant count.
-- Returns 404 when the room does not exist.
--
-- GET /room?room=<name>&domain=<base>[&subdomain=<sub>][&token=<jwt>]
-- Returns a JSON array of occupant objects {jid, email, display_name},
-- excluding the hidden Jitsi focus participant.
-- Returns 404 when the room does not exist.
--
-- GET /sessions
-- Returns the total number of active Prosody client sessions as a plain
-- integer string.
--
-- The room address is built as <room>@<muc_domain_prefix>.<domain>, optionally
-- prefixed with [<subdomain>] for multi-tenant deployments.
-- Token-based JWT verification is optional and controlled by the
-- enable_roomsize_token_verification module option (default: false).

local jid = require "util.jid";
local it = require "util.iterators";
Expand All @@ -14,30 +35,70 @@ if not have_async then
return;
end

local async_handler_wrapper = module:require "util".async_handler_wrapper;
-- muc_domain_prefix is needed by the build_room_address fallback below.
local muc_domain_prefix
= module:get_option_string("muc_mapper_domain_prefix", "conference");

-- Load shared utility library. If it fails (e.g. a transitive dependency is
-- missing in the current environment) log the error and fall back to inline
-- implementations so the HTTP routes are always registered.
local async_handler_wrapper, get_room_from_jid, build_room_address, is_focus;
local ok_util, util_or_err = pcall(function() return module:require "util" end);
if ok_util then
local util = util_or_err;
async_handler_wrapper = util.async_handler_wrapper;
get_room_from_jid = util.get_room_from_jid;
build_room_address = util.build_room_address;
is_focus = util.is_focus;
else
module:log("warn", "mod_muc_size: util.lib.lua unavailable (%s); using inline fallbacks",
tostring(util_or_err));
async_handler_wrapper = function(_, handler) return handler(_) end;
get_room_from_jid = function(room_jid)
local _, host = jid.split(room_jid);
local component = hosts[host];
if component then
local muc = component.modules.muc;
if muc then return muc.get_room_from_jid(room_jid) end
end
end;
build_room_address = function(room_name, domain_name, subdomain)
local addr = jid.join(room_name, muc_domain_prefix.."."..domain_name);
if subdomain and subdomain ~= "" then
addr = "["..subdomain.."]"..addr;
end
return addr;
end;
is_focus = function(nick)
return string.sub(nick, -string.len("/focus")) == "/focus";
end;
end

local tostring = tostring;
local neturl = require "net.url";
local parse = neturl.parseQuery;

-- Simple query-string parser: "room=foo&domain=bar" → { room="foo", domain="bar" }.
-- Avoids a dependency on the optional third-party net.url LuaRocks module.
local function parse(q)
local t = {};
for k, v in (q or ""):gmatch("([^=&]+)=([^&]*)") do
t[k] = v;
end
return t;
end

-- 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;
local ok, token_util_mod = pcall(function() return module:require "token/util" end);
local token_util = ok and token_util_mod and token_util_mod.new(module) or nil;

-- 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
Expand Down Expand Up @@ -82,44 +143,39 @@ function handle_get_room_size(event)
return { status_code = 400; };
end

local params = parse(event.request.url.query);
local room_name = params["room"];
local domain_name = params["domain"];
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
local room_address = build_room_address(room_name, domain_name, subdomain);

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 room = get_room_from_jid(room_address);
local participant_count = 0;

log("debug", "Querying room %s", tostring(room_address));
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",
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
else
log("debug", "no such room exists");
return { status_code = 404; };
end

if participant_count > 1 then
participant_count = participant_count - 1;
end
if participant_count > 1 then
participant_count = participant_count - 1;
end

return { status_code = 200; body = [[{"participants":]]..participant_count..[[}]] };
return { status_code = 200; body = [[{"participants":]]..participant_count..[[}]] };
end

--- Handles request for retrieving the room participants details
Expand All @@ -130,68 +186,60 @@ function handle_get_room (event)
return { status_code = 400; };
end

local params = parse(event.request.url.query);
local room_name = params["room"];
local domain_name = params["domain"];
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
local room_address = build_room_address(room_name, domain_name, subdomain);

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",
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 not is_focus(occupant.nick) 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
else
log("debug", "no such room exists");
return { status_code = 404; };
end

if participant_count > 1 then
participant_count = participant_count - 1;
end
if participant_count > 1 then
participant_count = participant_count - 1;
end

return { status_code = 200; body = json.encode(occupants_json); };
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

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