diff --git a/resources/prosody-plugins/mod_muc_poltergeist.lua b/resources/prosody-plugins/mod_muc_poltergeist.lua index 8e8db4e53..2078e7495 100644 --- a/resources/prosody-plugins/mod_muc_poltergeist.lua +++ b/resources/prosody-plugins/mod_muc_poltergeist.lua @@ -5,13 +5,44 @@ local neturl = require "net.url"; local parse = neturl.parseQuery; local st = require "util.stanza"; local get_room_from_jid = module:require "util".get_room_from_jid; +local wrap_async_run = module:require "util".wrap_async_run; +local timer = require "util.timer"; -- Options local poltergeist_component = module:get_option_string("poltergeist_component", module.host); +-- defaults to 3 min +local poltergeist_timeout + = module:get_option_string("poltergeist_leave_timeout", 180); +-- 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); + +-- option to expire poltergeist with custom status text +local poltergeistExpiredStatus + = module:get_option_string("poltergeist_expired_status"); -- table to store all poltergeists we create local poltergeists = {}; +-- table to mark that outgoing unavailable presences +-- should be marked with ignore +local poltergeists_pr_ignore = {}; -- poltergaist management functions @@ -21,8 +52,9 @@ local poltergeists = {}; -- @return returns room if found or nil function get_room(room_name, group) local room_address = jid.join(room_name, module:get_host()); - -- if there is a group we are in multidomain mode - if group and group ~= "" then + -- 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 @@ -59,6 +91,67 @@ function get_username(room, user_id) return poltergeists[room_name][user_id]; end +-- Removes poltergeist values from table +-- @param room the room instance +-- @param nick the user nick +function remove_username(room, nick) + local room_name = jid.node(room.jid); + if (poltergeists[room_name]) then + local user_id_to_remove; + for name,username in pairs(poltergeists[room_name]) do + if (string.sub(username, 0, 8) == nick) then + user_id_to_remove = name; + end + end + if (user_id_to_remove) then + poltergeists[room_name][user_id_to_remove] = nil; + end + end +end + +--- 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) +-- @return true if values are ok or false otherwise +function verify_token(token, room_name, group) + 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 + + 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 + + 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 + -- 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 @@ -67,7 +160,7 @@ prosody.events.add_handler("pre-jitsi-authentication", function(session) if (session.jitsi_meet_context_user) then local room = get_room( session.jitsi_bosh_query_room, - session.jitsi_meet_context_group); + session.jitsi_meet_domain); if (not room) then return nil; @@ -88,7 +181,7 @@ prosody.events.add_handler("pre-jitsi-authentication", function(session) -- we will mark it with ignore tag local nick = string.sub(username, 0, 8); if (have_poltergeist_occupant(room, nick)) then - remove_poltergeist_occupant(room, nick); + remove_poltergeist_occupant(room, nick, true); end return username; @@ -102,7 +195,8 @@ end); -- @param nick the nick to use for the new occupant -- @param name the display name fot the occupant (optional) -- @param avatar the avatar to use for the new occupant (optional) -function create_poltergeist_occupant(room, nick, name, avatar) +-- @param status the initial status to use for the new occupant (optional) +function create_poltergeist_occupant(room, nick, name, avatar, status) log("debug", "create_poltergeist_occupant %s:", nick); -- Join poltergeist occupant to room, with the invited JID as their nick local join_presence = st.presence({ @@ -118,22 +212,103 @@ function create_poltergeist_occupant(room, nick, name, avatar) if (avatar) then join_presence:tag("avatar-url"):text(avatar):up(); end + if (status) then + join_presence:tag("status"):text(status):up(); + end room:handle_first_presence( prosody.hosts[poltergeist_component], join_presence); + + local timeout = poltergeist_timeout; + -- the timeout before removing so participants can see the status update + local removeTimeout = 5; + if (poltergeistExpiredStatus) then + timeout = timeout - removeTimeout; + end + + timer.add_task(timeout, + function () + if (poltergeistExpiredStatus) then + update_poltergeist_occupant_status( + room, nick, poltergeistExpiredStatus); + -- and remove it after some time so participant can see + -- the update + timer.add_task(removeTimeout, + function () + if (have_poltergeist_occupant(room, nick)) then + remove_poltergeist_occupant(room, nick, false); + end + end); + else + if (have_poltergeist_occupant(room, nick)) then + remove_poltergeist_occupant(room, nick, false); + end + end + end); end -- Removes poltergeist occupant -- @param room the room instance where to remove the occupant -- @param nick the nick of the occupant to remove -function remove_poltergeist_occupant(room, nick) +-- @param ignore to mark the poltergeist unavailble presence to be ignored +function remove_poltergeist_occupant(room, nick, ignore) log("debug", "remove_poltergeist_occupant %s", nick); local leave_presence = st.presence({ to = room.jid.."/"..nick, from = poltergeist_component.."/"..nick, type = "unavailable" }); + if (ignore) then + poltergeists_pr_ignore[room.jid.."/"..nick] = true; + end room:handle_normal_presence( prosody.hosts[poltergeist_component], leave_presence); + remove_username(room, nick); +end + +-- Updates poltergeist occupant status +-- @param room the room instance where to remove the occupant +-- @param nick the nick of the occupant to remove +-- @param status the status to update +function update_poltergeist_occupant_status(room, nick, status) + local update_presence = get_presence(room, nick); + + if (not update_presence) then + -- no presence found for occupant, create one + update_presence = st.presence({ + to = room.jid.."/"..nick, + from = poltergeist_component.."/"..nick + }); + else + -- update occupant presence with appropriate to and from + -- so we can send it again + update_presence = st.clone(update_presence); + update_presence.attr.to = room.jid.."/"..nick; + update_presence.attr.from = poltergeist_component.."/"..nick; + end + + local once = false; + -- the status tag we will attach + local statusTag = st.stanza("status"):text(status); + + -- if there is already a status tag replace it + update_presence:maptags(function (tag) + if tag.name == statusTag.name then + if not once then + once = true; + return statusTag; + else + return nil; + end + end + return tag; + end); + if (not once) then + -- no status tag was repleced, attach it + update_presence:add_child(statusTag); + end + + room:handle_normal_presence( + prosody.hosts[poltergeist_component], update_presence); end -- Checks for existance of a poltergeist occupant @@ -145,12 +320,26 @@ function have_poltergeist_occupant(room, nick) return not not room:get_occupant_jid(poltergeist_component.."/"..nick); end +-- Returns the last presence of occupant +-- @param room the room instance where to check for occupant +-- @param nick the nick of the occupant +-- @return presence of the occupant +function get_presence(room, nick) + local occupant_jid + = room:get_occupant_jid(poltergeist_component.."/"..nick); + if (occupant_jid) then + return room:get_occupant_by_nick(occupant_jid):get_presence(); + end + + return nil; +end + -- Event handlers --- 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) - remove_poltergeist_occupant(event.room, bare(event.stanza.attr.from)); + remove_poltergeist_occupant(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 @@ -158,34 +347,54 @@ end, -100); -- 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") then + if(event.stanza.attr.type == "unavailable" + and poltergeists_pr_ignore[event.occupant.nick]) then event.stanza:tag( "ignore", { xmlns = "http://jitsi.org/jitmeet/" }):up(); + poltergeists_pr_ignore[event.occupant.nick] = nil; end end end, -100); +-- cleanup room table after room is destroyed +module:hook("muc-room-destroyed",function(event) + local room_name = jid.node(event.room.jid); + if (poltergeists[room_name]) then + poltergeists[room_name] = nil; + end +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 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"]; + + if not verify_token(params["token"], room_name, group) then + return 403; + end local room = get_room(room_name, group); if (not room) then - log("error", "no room found %s", room_address); + log("error", "no room found %s", room_name); return 404; end local username = generate_uuid(); store_username(room, user_id, username) - create_poltergeist_occupant(room, string.sub(username,0,8), name, avatar); + create_poltergeist_occupant( + room, string.sub(username,0,8), name, avatar, status); return 200; end @@ -194,15 +403,23 @@ end -- @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 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"]; + if not verify_token(params["token"], room_name, group) then + return 403; + end + local room = get_room(room_name, group); if (not room) then - log("error", "no room found %s", room_address); + log("error", "no room found %s", room_name); return 404; end @@ -213,20 +430,49 @@ function handle_update_poltergeist (event) local nick = string.sub(username, 0, 8); if (have_poltergeist_occupant(room, nick)) then - local update_presence = st.presence({ - to = room.jid.."/"..nick, - from = poltergeist_component.."/"..nick - }):tag("status"):text(status):up(); - - room:handle_normal_presence( - prosody.hosts[poltergeist_component], update_presence); - + update_poltergeist_occupant_status(room, nick, status); return 200; else return 404; end 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 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 403; + end + + local room = get_room(room_name, group); + if (not room) then + log("error", "no room found %s", room_name); + return 404; + end + + local username = get_username(room, user_id); + if (not username) then + return 404; + end + + local nick = string.sub(username, 0, 8); + if (have_poltergeist_occupant(room, nick)) then + remove_poltergeist_occupant(room, nick, false); + return 200; + else + return 404; + end +end log("info", "Loading poltergeist service"); module:depends("http"); @@ -234,7 +480,8 @@ module:provides("http", { default_path = "/"; name = "poltergeist"; route = { - ["GET /poltergeist/create"] = handle_create_poltergeist; - ["GET /poltergeist/update"] = handle_update_poltergeist; + ["GET /poltergeist/create"] = function (event) return wrap_async_run(event,handle_create_poltergeist) end; + ["GET /poltergeist/update"] = function (event) return wrap_async_run(event,handle_update_poltergeist) end; + ["GET /poltergeist/remove"] = function (event) return wrap_async_run(event,handle_remove_poltergeist) end; }; }); diff --git a/resources/prosody-plugins/mod_muc_size.lua b/resources/prosody-plugins/mod_muc_size.lua index d8d2716c5..921e7b37b 100644 --- a/resources/prosody-plugins/mod_muc_size.lua +++ b/resources/prosody-plugins/mod_muc_size.lua @@ -9,6 +9,7 @@ local it = require "util.iterators"; local json = require "util.json"; local iterators = require "util.iterators"; local array = require"util.array"; +local wrap_async_run = module:require "util".wrap_async_run; local tostring = tostring; local neturl = require "net.url"; @@ -72,6 +73,10 @@ end -- @return GET response, containing a json with participants count, -- tha value is without counting the focus. function handle_get_room_size(event) + if (not event.request.url.query) then + return 400; + end + local params = parse(event.request.url.query); local room_name = params["room"]; local domain_name = params["domain"]; @@ -121,6 +126,10 @@ end -- @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 400; + end + local params = parse(event.request.url.query); local room_name = params["room"]; local domain_name = params["domain"]; @@ -184,9 +193,9 @@ function module.load() module:provides("http", { default_path = "/"; route = { - ["GET room-size"] = handle_get_room_size; + ["GET room-size"] = function (event) return wrap_async_run(event,handle_get_room_size) end; ["GET sessions"] = function () return tostring(it.count(it.keys(prosody.full_sessions))); end; - ["GET room"] = handle_get_room; + ["GET room"] = function (event) return wrap_async_run(event,handle_get_room) end; }; }); end diff --git a/resources/prosody-plugins/util.lib.lua b/resources/prosody-plugins/util.lib.lua index b0a6a38f0..209d65196 100644 --- a/resources/prosody-plugins/util.lib.lua +++ b/resources/prosody-plugins/util.lib.lua @@ -1,4 +1,5 @@ local jid = require "util.jid"; +local runner, waiter = require "util.async".runner, require "util.async".waiter; --- Finds and returns room by its jid -- @param room_jid the room jid to search in the muc component @@ -20,6 +21,20 @@ function get_room_from_jid(room_jid) end end + +function wrap_async_run(event,handler) + local result; + local async_func = runner(function (event) + local wait, done = waiter(); + result=handler(event); + done(); + return result; + end) + async_func:run(event) + return result; +end + return { get_room_from_jid = get_room_from_jid; + wrap_async_run = wrap_async_run; }; \ No newline at end of file