From 4d51aedde0db3cce9158dc816a0d7519fe491927 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=B0=D0=BC=D1=8F=D0=BD=20=D0=9C=D0=B8=D0=BD=D0=BA?= =?UTF-8?q?=D0=BE=D0=B2?= Date: Tue, 12 Jul 2022 09:51:13 +0300 Subject: [PATCH] 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. --- .../prosody.cfg.lua-jvb.example | 1 + doc/debian/jitsi-meet/jitsi-meet.example | 15 ++ .../mod_muc_password_check.lua | 191 ++++++++++++++++++ resources/prosody-plugins/token/util.lib.lua | 9 +- resources/prosody-plugins/util.lib.lua | 6 + 5 files changed, 218 insertions(+), 4 deletions(-) create mode 100644 resources/prosody-plugins/mod_muc_password_check.lua diff --git a/doc/debian/jitsi-meet-prosody/prosody.cfg.lua-jvb.example b/doc/debian/jitsi-meet-prosody/prosody.cfg.lua-jvb.example index 40da4afcf..f5b3d2116 100644 --- a/doc/debian/jitsi-meet-prosody/prosody.cfg.lua-jvb.example +++ b/doc/debian/jitsi-meet-prosody/prosody.cfg.lua-jvb.example @@ -145,6 +145,7 @@ VirtualHost "jigasi.meet.jitsi" modules_enabled = { "ping"; "bosh"; + "muc_password_check"; } authentication = "token" app_id = "jitsi"; diff --git a/doc/debian/jitsi-meet/jitsi-meet.example b/doc/debian/jitsi-meet/jitsi-meet.example index 66f0eca8d..33c6ab5e4 100644 --- a/doc/debian/jitsi-meet/jitsi-meet.example +++ b/doc/debian/jitsi-meet/jitsi-meet.example @@ -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."; diff --git a/resources/prosody-plugins/mod_muc_password_check.lua b/resources/prosody-plugins/mod_muc_password_check.lua new file mode 100644 index 000000000..da55c2c16 --- /dev/null +++ b/resources/prosody-plugins/mod_muc_password_check.lua @@ -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); diff --git a/resources/prosody-plugins/token/util.lib.lua b/resources/prosody-plugins/token/util.lib.lua index 7504e8114..56c5c0c53 100644 --- a/resources/prosody-plugins/token/util.lib.lua +++ b/resources/prosody-plugins/token/util.lib.lua @@ -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 diff --git a/resources/prosody-plugins/util.lib.lua b/resources/prosody-plugins/util.lib.lua index 3203fe120..2f231b9cb 100644 --- a/resources/prosody-plugins/util.lib.lua +++ b/resources/prosody-plugins/util.lib.lua @@ -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; };