feat: Adds room info http endpoint jwt protected. (#11738)

* feat: Adds room info http endpoint jwt protected.

Used from dialplan from jigasi for handling passwords in IVR.

* squash: Fixes comments.

* squash: nginx api/rom-info

* fix: Skips tenant checks when enableDomainVerification is false.

* squash: Drops duplicate code and supports multi-shards.

By adding room= parameter in query and tenant prefix for the api we add support for multi-shards setup.

* feat: Enable domain verification by default.

This is used when verifying room access with token_verification module.

* squash: Update docs.
This commit is contained in:
Дамян Минков 2022-07-12 09:51:13 +03:00 committed by GitHub
parent 058c82a704
commit 4d51aedde0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 218 additions and 4 deletions

View File

@ -145,6 +145,7 @@ VirtualHost "jigasi.meet.jitsi"
modules_enabled = {
"ping";
"bosh";
"muc_password_check";
}
authentication = "token"
app_id = "jitsi";

View File

@ -73,6 +73,13 @@ server {
alias /usr/share/jitsi-meet/libs/external_api.min.js;
}
location = /_api/room-info {
proxy_pass http://prosody/room-info?prefix=$prefix&$args;
proxy_http_version 1.1;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header Host $http_host;
}
# ensure all static content can always be found first
location ~ ^/(libs|css|static|images|fonts|lang|sounds|connection_optimization|.well-known)/(.*)$
{
@ -156,6 +163,14 @@ server {
rewrite ^/(.*)$ /xmpp-websocket;
}
location ~ ^/([^/?&:'"]+)/_api/room-info {
set $subdomain "$1.";
set $subdir "$1/";
set $prefix "$1";
rewrite ^/(.*)$ /_api/room-info;
}
# Anything that didn't match above, and isn't a real file, assume it's a room name and redirect to /
location ~ ^/([^/?&:'"]+)/(.*)$ {
set $subdomain "$1.";

View File

@ -0,0 +1,191 @@
local inspect = require "inspect";
local formdecode = require "util.http".formdecode;
local urlencode = require "util.http".urlencode;
local jid = require "util.jid";
local json = require "util.json";
local util = module:require "util";
local async_handler_wrapper = util.async_handler_wrapper;
local starts_with = util.starts_with;
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 = json.decode(request.body);
if not params then
module:log("warn", "Missing params");
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 PUT_response = {
headers = { content_type = "application/json"; };
body = json.encode({ valid = (room:get_password() == passcode) })
};
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 GET_response = {
headers = {
content_type = "application/json";
};
body = json.encode(room_details);
};
module:log("debug","Sending response for room password: %s", inspect(GET_response));
return GET_response;
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
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);

View File

@ -10,6 +10,7 @@ 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;
@ -68,9 +69,9 @@ function Util.new(module)
"muc_mapper_domain",
self.muc_domain_prefix.."."..self.muc_domain_base);
end
-- whether domain name verification is enabled, by default it is disabled
self.enableDomainVerification = module:get_option_boolean(
"enable_domain_verification", false);
-- 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");
@ -293,7 +294,7 @@ function Util:verify_room(session, room_address)
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 auth_room ~= '*' then
if auth_room and (room ~= auth_room and not ends_with(room, ']'..auth_room)) and auth_room ~= '*' then
return false;
end

View File

@ -254,6 +254,10 @@ function starts_with(str, start)
return str:sub(1, #start) == start
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)
if starts_with(room_jid, "__jicofo-health-check") then
@ -366,4 +370,6 @@ return {
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;
};