"Fossies" - the Fresh Open Source Software Archive

Member "jitsi-meet-7689/resources/prosody-plugins/util.lib.lua" (5 Dec 2023, 16156 Bytes) of package /linux/misc/jitsi-meet-7689.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Lua source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file.

    1 local jid = require "util.jid";
    2 local timer = require "util.timer";
    3 local http = require "net.http";
    4 local cache = require "util.cache";
    5 
    6 local http_timeout = 30;
    7 local have_async, async = pcall(require, "util.async");
    8 local http_headers = {
    9     ["User-Agent"] = "Prosody ("..prosody.version.."; "..prosody.platform..")"
   10 };
   11 
   12 local muc_domain_prefix = module:get_option_string("muc_mapper_domain_prefix", "conference");
   13 
   14 -- defaults to module.host, the module that uses the utility
   15 local muc_domain_base = module:get_option_string("muc_mapper_domain_base", module.host);
   16 
   17 -- The "real" MUC domain that we are proxying to
   18 local muc_domain = module:get_option_string("muc_mapper_domain", muc_domain_prefix.."."..muc_domain_base);
   19 
   20 local escaped_muc_domain_base = muc_domain_base:gsub("%p", "%%%1");
   21 local escaped_muc_domain_prefix = muc_domain_prefix:gsub("%p", "%%%1");
   22 -- The pattern used to extract the target subdomain
   23 -- (e.g. extract 'foo' from 'conference.foo.example.com')
   24 local target_subdomain_pattern = "^"..escaped_muc_domain_prefix..".([^%.]+)%."..escaped_muc_domain_base;
   25 
   26 -- table to store all incoming iqs without roomname in it, like discoinfo to the muc component
   27 local roomless_iqs = {};
   28 
   29 local split_subdomain_cache = cache.new(1000);
   30 local extract_subdomain_cache = cache.new(1000);
   31 local internal_room_jid_cache = cache.new(1000);
   32 
   33 local moderated_subdomains = module:get_option_set("allowners_moderated_subdomains", {})
   34 local moderated_rooms = module:get_option_set("allowners_moderated_rooms", {})
   35 
   36 -- Utility function to split room JID to include room name and subdomain
   37 -- (e.g. from room1@conference.foo.example.com/res returns (room1, example.com, res, foo))
   38 local function room_jid_split_subdomain(room_jid)
   39     local ret = split_subdomain_cache:get(room_jid);
   40     if ret then
   41         return ret.node, ret.host, ret.resource, ret.subdomain;
   42     end
   43 
   44     local node, host, resource = jid.split(room_jid);
   45 
   46     local target_subdomain = host and host:match(target_subdomain_pattern);
   47     local cache_value = {node=node, host=host, resource=resource, subdomain=target_subdomain};
   48     split_subdomain_cache:set(room_jid, cache_value);
   49     return node, host, resource, target_subdomain;
   50 end
   51 
   52 --- Utility function to check and convert a room JID from
   53 --- virtual room1@conference.foo.example.com to real [foo]room1@conference.example.com
   54 -- @param room_jid the room jid to match and rewrite if needed
   55 -- @param stanza the stanza
   56 -- @return returns room jid [foo]room1@conference.example.com when it has subdomain
   57 -- otherwise room1@conference.example.com(the room_jid value untouched)
   58 local function room_jid_match_rewrite(room_jid, stanza)
   59     local node, _, resource, target_subdomain = room_jid_split_subdomain(room_jid);
   60     if not target_subdomain then
   61         -- module:log("debug", "No need to rewrite out 'to' %s", room_jid);
   62         return room_jid;
   63     end
   64     -- Ok, rewrite room_jid  address to new format
   65     local new_node, new_host, new_resource;
   66     if node then
   67         new_node, new_host, new_resource = "["..target_subdomain.."]"..node, muc_domain, resource;
   68     else
   69         -- module:log("debug", "No room name provided so rewriting only host 'to' %s", room_jid);
   70         new_host, new_resource = muc_domain, resource;
   71 
   72         if (stanza and stanza.attr and stanza.attr.id) then
   73             roomless_iqs[stanza.attr.id] = stanza.attr.to;
   74         end
   75     end
   76 
   77     return jid.join(new_node, new_host, new_resource);
   78 end
   79 
   80 -- Utility function to check and convert a room JID from real [foo]room1@muc.example.com to virtual room1@muc.foo.example.com
   81 local function internal_room_jid_match_rewrite(room_jid, stanza)
   82     -- first check for roomless_iqs
   83     if (stanza and stanza.attr and stanza.attr.id and roomless_iqs[stanza.attr.id]) then
   84         local result = roomless_iqs[stanza.attr.id];
   85         roomless_iqs[stanza.attr.id] = nil;
   86         return result;
   87     end
   88 
   89     local ret = internal_room_jid_cache:get(room_jid);
   90     if ret then
   91         return ret;
   92     end
   93 
   94     local node, host, resource = jid.split(room_jid);
   95     if host ~= muc_domain or not node then
   96         -- module:log("debug", "No need to rewrite %s (not from the MUC host)", room_jid);
   97         internal_room_jid_cache:set(room_jid, room_jid);
   98         return room_jid;
   99     end
  100 
  101     local target_subdomain, target_node = extract_subdomain(node);
  102     if not (target_node and target_subdomain) then
  103         -- module:log("debug", "Not rewriting... unexpected node format: %s", node);
  104         internal_room_jid_cache:set(room_jid, room_jid);
  105         return room_jid;
  106     end
  107 
  108     -- Ok, rewrite room_jid address to pretty format
  109     ret = jid.join(target_node, muc_domain_prefix..".".. target_subdomain.."."..muc_domain_base, resource);
  110     internal_room_jid_cache:set(room_jid, ret);
  111     return ret;
  112 end
  113 
  114 --- Finds and returns room by its jid
  115 -- @param room_jid the room jid to search in the muc component
  116 -- @return returns room if found or nil
  117 function get_room_from_jid(room_jid)
  118     local _, host = jid.split(room_jid);
  119     local component = hosts[host];
  120     if component then
  121         local muc = component.modules.muc
  122         if muc and rawget(muc,"rooms") then
  123             -- We're running 0.9.x or 0.10 (old MUC API)
  124             return muc.rooms[room_jid];
  125         elseif muc and rawget(muc,"get_room_from_jid") then
  126             -- We're running >0.10 (new MUC API)
  127             return muc.get_room_from_jid(room_jid);
  128         else
  129             return
  130         end
  131     end
  132 end
  133 
  134 -- Returns the room if available, work and in multidomain mode
  135 -- @param room_name the name of the room
  136 -- @param group name of the group (optional)
  137 -- @return returns room if found or nil
  138 function get_room_by_name_and_subdomain(room_name, subdomain)
  139     local room_address;
  140 
  141     -- if there is a subdomain we are in multidomain mode and that subdomain is not our main host
  142     if subdomain and subdomain ~= "" and subdomain ~= muc_domain_base then
  143         room_address = jid.join("["..subdomain.."]"..room_name, muc_domain);
  144     else
  145         room_address = jid.join(room_name, muc_domain);
  146     end
  147 
  148     return get_room_from_jid(room_address);
  149 end
  150 
  151 function async_handler_wrapper(event, handler)
  152     if not have_async then
  153         module:log("error", "requires a version of Prosody with util.async");
  154         return nil;
  155     end
  156 
  157     local runner = async.runner;
  158 
  159     -- Grab a local response so that we can send the http response when
  160     -- the handler is done.
  161     local response = event.response;
  162     local async_func = runner(
  163         function (event)
  164             local result = handler(event)
  165 
  166             -- If there is a status code in the result from the
  167             -- wrapped handler then add it to the response.
  168             if tonumber(result.status_code) ~= nil then
  169                 response.status_code = result.status_code
  170             end
  171 
  172             -- If there are headers in the result from the
  173             -- wrapped handler then add them to the response.
  174             if result.headers ~= nil then
  175                 response.headers = result.headers
  176             end
  177 
  178             -- Send the response to the waiting http client with
  179             -- or without the body from the wrapped handler.
  180             if result.body ~= nil then
  181                 response:send(result.body)
  182             else
  183                 response:send();
  184             end
  185         end
  186     )
  187     async_func:run(event)
  188     -- return true to keep the client http connection open.
  189     return true;
  190 end
  191 
  192 --- Updates presence stanza, by adding identity node
  193 -- @param stanza the presence stanza
  194 -- @param user the user to which presence we are updating identity
  195 -- @param group the group of the user to which presence we are updating identity
  196 -- @param creator_user the user who created the user which presence we
  197 -- are updating (this is the poltergeist case, where a user creates
  198 -- a poltergeist), optional.
  199 -- @param creator_group the group of the user who created the user which
  200 -- presence we are updating (this is the poltergeist case, where a user creates
  201 -- a poltergeist), optional.
  202 function update_presence_identity(
  203     stanza, user, group, creator_user, creator_group)
  204 
  205     -- First remove any 'identity' element if it already
  206     -- exists, so it cannot be spoofed by a client
  207     stanza:maptags(
  208         function(tag)
  209             for k, v in pairs(tag) do
  210                 if k == "name" and v == "identity" then
  211                     return nil
  212                 end
  213             end
  214             return tag
  215         end
  216     )
  217 
  218     stanza:tag("identity"):tag("user");
  219     for k, v in pairs(user) do
  220         v = tostring(v)
  221         stanza:tag(k):text(v):up();
  222     end
  223     stanza:up();
  224 
  225     -- Add the group information if it is present
  226     if group then
  227         stanza:tag("group"):text(group):up();
  228     end
  229 
  230     -- Add the creator user information if it is present
  231     if creator_user then
  232         stanza:tag("creator_user");
  233         for k, v in pairs(creator_user) do
  234             stanza:tag(k):text(v):up();
  235         end
  236         stanza:up();
  237 
  238         -- Add the creator group information if it is present
  239         if creator_group then
  240             stanza:tag("creator_group"):text(creator_group):up();
  241         end
  242     end
  243 
  244     stanza:up(); -- Close identity tag
  245 end
  246 
  247 -- Utility function to check whether feature is present and enabled. Allow
  248 -- a feature if there are features present in the session(coming from
  249 -- the token) and the value of the feature is true.
  250 -- If features is not present in the token we skip feature detection and allow
  251 -- everything.
  252 function is_feature_allowed(features, ft)
  253     if (features == nil or features[ft] == "true" or features[ft] == true) then
  254         return true;
  255     else
  256         return false;
  257     end
  258 end
  259 
  260 --- Extracts the subdomain and room name from internal jid node [foo]room1
  261 -- @return subdomain(optional, if extracted or nil), the room name
  262 function extract_subdomain(room_node)
  263     local ret = extract_subdomain_cache:get(room_node);
  264     if ret then
  265         return ret.subdomain, ret.room;
  266     end
  267 
  268     local subdomain, room_name = room_node:match("^%[([^%]]+)%](.+)$");
  269     local cache_value = {subdomain=subdomain, room=room_name};
  270     extract_subdomain_cache:set(room_node, cache_value);
  271     return subdomain, room_name;
  272 end
  273 
  274 function starts_with(str, start)
  275     return str:sub(1, #start) == start
  276 end
  277 
  278 function ends_with(str, ending)
  279     return ending == "" or str:sub(-#ending) == ending
  280 end
  281 
  282 -- healthcheck rooms in jicofo starts with a string '__jicofo-health-check'
  283 function is_healthcheck_room(room_jid)
  284     return starts_with(room_jid, "__jicofo-health-check");
  285 end
  286 
  287 --- Utility function to make an http get request and
  288 --- retry @param retry number of times
  289 -- @param url endpoint to be called
  290 -- @param retry nr of retries, if retry is
  291 -- @param auth_token value to be passed as auth Bearer
  292 -- nil there will be no retries
  293 -- @returns result of the http call or nil if
  294 -- the external call failed after the last retry
  295 function http_get_with_retry(url, retry, auth_token)
  296     local content, code, cache_for;
  297     local timeout_occurred;
  298     local wait, done = async.waiter();
  299     local request_headers = http_headers or {}
  300     if auth_token ~= nil then
  301         request_headers['Authorization'] = 'Bearer ' .. auth_token
  302     end
  303 
  304     local function cb(content_, code_, response_, request_)
  305         if timeout_occurred == nil then
  306             code = code_;
  307             if code == 200 or code == 204 then
  308                 module:log("debug", "External call was successful, content %s", content_);
  309                 content = content_;
  310 
  311                 -- if there is cache-control header, let's return the max-age value
  312                 if response_ and response_.headers and response_.headers['cache-control'] then
  313                     local vals = {};
  314                     for k, v in response_.headers['cache-control']:gmatch('(%w+)=(%w+)') do
  315                       vals[k] = v;
  316                     end
  317                     -- max-age=123 will be parsed by the regex ^ to age=123
  318                     cache_for = vals.age;
  319                 end
  320             else
  321                 module:log("warn", "Error on GET request: Code %s, Content %s",
  322                     code_, content_);
  323             end
  324             done();
  325         else
  326             module:log("warn", "External call reply delivered after timeout from: %s", url);
  327         end
  328     end
  329 
  330     local function call_http()
  331         return http.request(url, {
  332             headers = request_headers,
  333             method = "GET"
  334         }, cb);
  335     end
  336 
  337     local request = call_http();
  338 
  339     local function cancel()
  340         -- TODO: This check is racey. Not likely to be a problem, but we should
  341         --       still stick a mutex on content / code at some point.
  342         if code == nil then
  343             timeout_occurred = true;
  344             module:log("warn", "Timeout %s seconds making the external call to: %s", http_timeout, url);
  345             -- no longer present in prosody 0.11, so check before calling
  346             if http.destroy_request ~= nil then
  347                 http.destroy_request(request);
  348             end
  349             if retry == nil then
  350                 module:log("debug", "External call failed and retry policy is not set");
  351                 done();
  352             elseif retry ~= nil and retry < 1 then
  353                 module:log("debug", "External call failed after retry")
  354                 done();
  355             else
  356                 module:log("debug", "External call failed, retry nr %s", retry)
  357                 retry = retry - 1;
  358                 request = call_http()
  359                 return http_timeout;
  360             end
  361         end
  362     end
  363     timer.add_task(http_timeout, cancel);
  364     wait();
  365 
  366     return content, code, cache_for;
  367 end
  368 
  369 -- Checks whether there is status in the <x node
  370 -- @param muc_x the <x element from presence
  371 -- @param status checks for this status
  372 -- @returns true if the status is found, false otherwise or if no muc_x is provided.
  373 function presence_check_status(muc_x, status)
  374     if not muc_x then
  375         return false;
  376     end
  377 
  378     for statusNode in muc_x:childtags('status') do
  379         if statusNode.attr.code == status then
  380             return true;
  381         end
  382     end
  383 
  384     return false;
  385 end
  386 
  387 -- Retrieves the focus from the room and cache it in the room object
  388 -- @param room The room name for which to find the occupant
  389 local function get_focus_occupant(room)
  390     return room:get_occupant_by_nick(room.jid..'/focus');
  391 end
  392 
  393 -- Checks whether the jid is moderated, the room name is in moderated_rooms
  394 -- or if the subdomain is in the moderated_subdomains
  395 -- @return returns on of the:
  396 --      -> false
  397 --      -> true, room_name, subdomain
  398 --      -> true, room_name, nil (if no subdomain is used for the room)
  399 function is_moderated(room_jid)
  400     if moderated_subdomains:empty() and moderated_rooms:empty() then
  401         return false;
  402     end
  403 
  404     local room_node = jid.node(room_jid);
  405     -- parses bare room address, for multidomain expected format is:
  406     -- [subdomain]roomName@conference.domain
  407     local target_subdomain, target_room_name = extract_subdomain(room_node);
  408     if target_subdomain then
  409         if moderated_subdomains:contains(target_subdomain) then
  410             return true, target_room_name, target_subdomain;
  411         end
  412     elseif moderated_rooms:contains(room_node) then
  413         return true, room_node, nil;
  414     end
  415 
  416     return false;
  417 end
  418 
  419 return {
  420     extract_subdomain = extract_subdomain;
  421     is_feature_allowed = is_feature_allowed;
  422     is_healthcheck_room = is_healthcheck_room;
  423     is_moderated = is_moderated;
  424     get_focus_occupant = get_focus_occupant;
  425     get_room_from_jid = get_room_from_jid;
  426     get_room_by_name_and_subdomain = get_room_by_name_and_subdomain;
  427     async_handler_wrapper = async_handler_wrapper;
  428     presence_check_status = presence_check_status;
  429     room_jid_match_rewrite = room_jid_match_rewrite;
  430     room_jid_split_subdomain = room_jid_split_subdomain;
  431     internal_room_jid_match_rewrite = internal_room_jid_match_rewrite;
  432     update_presence_identity = update_presence_identity;
  433     http_get_with_retry = http_get_with_retry;
  434     ends_with = ends_with;
  435     starts_with = starts_with;
  436 };