123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583 |
- local bare = require "util.jid".bare;
- local generate_uuid = require "util.uuid".generate;
- local jid = require "util.jid";
- 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 update_presence_identity = module:require "util".update_presence_identity;
- local timer = require "util.timer";
- local MUC_NS = "http://jabber.org/protocol/muc";
- local expired_status = "expired";
-
- -- Options
- local poltergeist_component
- = module:get_option_string("poltergeist_component", module.host);
- -- defaults to 30 seconds
- local poltergeist_timeout
- = module:get_option_string("poltergeist_leave_timeout", 30);
- -- 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);
-
- -- 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
-
- -- Returns the room if available, work and in multidomain mode
- -- @param room_name the name of the room
- -- @param group name of the group (optional)
- -- @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 and that group is not
- -- our parent host
- if group and group ~= "" and group ~= parentHostName then
- room_address = "["..group.."]"..room_address;
- end
-
- return get_room_from_jid(room_address);
- end
-
- -- Stores the username in the table where we store poltergeist usernames
- -- based on their room names
- -- @param room the room instance
- -- @param user_id the user id
- -- @param username the username to store
- function store_username(room, user_id, username)
- local room_name = jid.node(room.jid);
-
- -- we store in poltergeist user ids for room names
- if (not poltergeists[room_name]) then
- poltergeists[room_name] = {};
- end
- poltergeists[room_name][user_id] = username;
- log("debug", "stored in session: %s", username);
- end
-
- -- Retrieve the username for a user
- -- @param room the room instance
- -- @param user_id the user id
- -- @return returns the stored username for user or nil
- function get_username(room, user_id)
- local room_name = jid.node(room.jid);
-
- if (not poltergeists[room_name]) then
- return nil;
- end
-
- 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)
- -- @param session the session to use for storing token specific fields
- -- @return true if values are ok or false otherwise
- function verify_token(token, room_name, group, session)
- 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
-
- 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
- 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_domain);
-
- if (not room) then
- return nil;
- end
-
- local username
- = get_username(room, session.jitsi_meet_context_user["id"]);
-
- if (not username) then
- return nil;
- end
-
- log("debug", "Found predefined username %s", username);
-
- -- let's find the room and if the poltergeist occupant is there
- -- lets remove him before the real participant joins
- -- when we see the unavailable presence to go out the server
- -- we will mark it with ignore tag
- local nick = string.sub(username, 0, 8);
- if (have_poltergeist_occupant(room, nick)) then
- -- notify that user connected using the poltergeist
- update_poltergeist_occupant_status(
- room, nick, "connected");
- remove_poltergeist_occupant(room, nick, true);
- end
-
- return username;
- end
-
- return nil;
- end);
-
- -- Removes poltergeist occupant
- -- @param room the room instance where to remove the occupant
- -- @param nick the nick of the occupant to remove
- -- @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 current_presence = get_presence(room, nick);
- if (not current_presence) then
- module:log("info", "attempted to remove a poltergeist with no presence")
- return;
- end
-
- local leave_presence = st.clone(current_presence)
- leave_presence.attr.to = room.jid.."/"..nick;
- leave_presence.attr.from = poltergeist_component.."/"..nick;
- leave_presence.attr.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
- -- @param call_details is a table of call flow details
- function update_poltergeist_occupant_status(room, nick, status, call_details)
- local update_presence = get_presence(room, nick);
-
- if (not update_presence) then
- -- TODO: determine if we should provide an error and how that would be
- -- handled for bosh and http api.
- module:log("info", "update issued for a non-existing poltergeist")
- return;
- end
-
- -- 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;
-
- update_presence = update_presence_tags(update_presence, status, call_details)
-
- room:handle_normal_presence(
- prosody.hosts[poltergeist_component], update_presence);
- end
-
- -- Updates the status tags and call flow tags of an existing poltergeist's
- -- presence.
- -- @param presence_stanza is the actual presence stanza for a poltergeist.
- -- @param status is the new status to be updated in the stanza.
- -- @param call_details is a table of call flow signal information.
- function update_presence_tags(presence_stanza, status, call_details)
- local call_cancel = false;
- local call_id = nil;
-
- -- Extract optional call flow signal information.
- if call_details then
- call_id = call_details["id"];
-
- if call_details["cancel"] then
- call_cancel = call_details["cancel"];
- end
- end
-
- presence_stanza:maptags(function (tag)
- if tag.name == "status" then
- if call_cancel then
- -- If call cancel is set then the status should not be changed.
- return tag
- end
- return st.stanza("status"):text(status);
- elseif tag.name == "call_id" then
- if call_id then
- return st.stanza("call_id"):text(call_id);
- else
- -- If no call id is provided the re-use the existing id.
- return tag;
- end
- elseif tag.name == "call_cancel" then
- if call_cancel then
- return st.stanza("call_cancel"):text("true");
- else
- return st.stanza("call_cancel"):text("false");
- end
- end
- return tag;
- end);
-
- return presence_stanza
- end
-
- -- Checks for existance of a poltergeist occupant
- -- @param room the room instance where to check for occupant
- -- @param nick the nick of the occupant
- -- @return true if occupant is found, false otherwise
- function have_poltergeist_occupant(room, nick)
- -- Find out if we have a poltergeist occupant in the room for this JID
- 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), 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
- -- we ignore this presence to avoid leaving/joining experience and the real
- -- 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"
- 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"];
- local session = {};
-
- if not verify_token(params["token"], room_name, group, session) then
- return 403;
- end
-
- -- If the provided room conference doesn't exist then we
- -- can't add a poltergeist to it.
- local room = get_room(room_name, group);
- if (not room) then
- log("error", "no room found %s", room_name);
- return 404;
- end
-
- -- If the poltergiest is already in the conference then it will
- -- be in our username store and another can't be added.
- local username = get_username(room, user_id);
- if (username ~= nil
- and have_poltergeist_occupant(room, string.sub(username, 0, 8))) then
- log("warn", "poltergeist for username:%s already in the room:%s",
- username, room_name);
- return 202;
- end
- username = generate_uuid();
- local context = {
- user = {
- id = user_id;
- };
- group = group;
- creator_user = session.jitsi_meet_context_user;
- creator_group = session.jitsi_meet_context_group;
- };
-
- local nick = string.sub(username, 0, 8)
- local presence_stanza = original_presence(
- poltergeist_component,
- room,
- nick,
- name,
- avatar,
- username,
- context,
- status
- )
- store_username(room, user_id, username);
-
- room:handle_first_presence(
- prosody.hosts[poltergeist_component],
- presence_stanza
- );
-
- -- the timeout before removing so participants can see the status update
- local removeTimeout = 5;
- local timeout = poltergeist_timeout - removeTimeout;
-
- timer.add_task(timeout,
- function ()
- update_poltergeist_occupant_status(
- room, nick, expired_status);
- -- 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);
- end);
-
- return 200;
- end
-
- -- Generate the original presence for a poltergeist when it is added to a room.
- -- @param component is the configured component name for poltergeist.
- -- @param room is the room the poltergeist is being added to.
- -- @param nick is the nick the poltergeist will use for xmpp.
- -- @param avatar is the url of the display avatar for the poltergeist.
- -- @param username is the poltergeist unique username.
- -- @param context is the context information from the valid auth token.
- -- @param status is the status string for the presence.
- -- @return a presence stanza
- function original_presence(
- component, room, nick, name, avatar, username, context, status)
- local p = st.presence({
- to = room.jid.."/"..nick,
- from = component.."/"..nick,
- }):tag("x", { xmlns = MUC_NS }):up();
-
- p:tag("bot", { type = "poltergeist" }):up();
- p:tag("call_cancel"):text(nil):up();
- p:tag("call_id"):text(username):up();
-
- if status then
- p:tag("status"):text(status):up();
- else
- p:tag("status"):text(nil):up();
- end
-
- if (name) then
- p:tag(
- "nick",
- { xmlns = "http://jabber.org/protocol/nick" }):text(name):up();
- end
-
- if (avatar) then
- p:tag("avatar-url"):text(avatar):up();
- end
-
- -- If the room has a password set, let the poltergeist enter using it
- local room_password = room:get_password();
- if room_password then
- local join = p:get_child("x", MUC_NS);
- join:tag("password", { xmlns = MUC_NS }):text(room_password);
- end
-
- update_presence_identity(
- p,
- context.user,
- context.group,
- context.creator_user,
- context.creator_group
- );
- return p
- end
-
- --- Handles request for updating poltergeists status
- -- @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"];
- local call_id = params["callid"];
-
- local call_cancel = false
- if params["callcancel"] == "true" then
- call_cancel = true;
- end
-
- 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 call_details = {
- ["cancel"] = call_cancel;
- ["id"] = call_id;
- };
-
- local nick = string.sub(username, 0, 8);
- if (not have_poltergeist_occupant(room, nick)) then
- return 404;
- end
-
- update_poltergeist_occupant_status(room, nick, status, call_details);
- return 200;
- 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 (not have_poltergeist_occupant(room, nick)) then
- return 404;
- end
-
- remove_poltergeist_occupant(room, nick, false);
- return 200;
- end
-
- log("info", "Loading poltergeist service");
- module:depends("http");
- module:provides("http", {
- default_path = "/";
- name = "poltergeist";
- route = {
- ["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;
- };
- });
|