prosody-modules/mod_mix_pam/mod_mix_pam.lua

352 lines
12 KiB
Lua
Raw Normal View History

2021-02-20 19:55:07 +00:00
local module_host = module:get_host();
--[[
2021-02-20 19:55:07 +00:00
if module_host ~= "*" then
module:log("error", "mix_pam should be used on the user host!");
end
]]--
local jid = require("util.jid");
local st = require("util.stanza");
local rm_remove_from_roster = require("core.rostermanager").remove_from_roster;
local rm_add_to_roster = require("core.rostermanager").add_to_roster;
local rm_roster_push = require("core.rostermanager").roster_push;
2021-02-21 11:29:33 +00:00
local rm_load_roster = require("core.rostermanager").load_roster;
-- Persistent storage
local mix_pam = module:open_store("mix_pam", "keyval");
-- Runtime data
2021-02-21 11:29:33 +00:00
local mix_hosts = {}; -- MIX host's JID -> Reference Counter
-- Namespaceing
local mix_pam_xmlns = "urn:xmpp:mix:pam:2";
local mix_roster_xmlns = "urn:xmpp:mix:roster:0";
module:add_feature(mix_pam_xmlns);
-- NOTE: To show that we archive messages
-- module:add_feature(mix_pam_xmlns.."#archive");
2021-02-20 19:55:07 +00:00
local function add_mix_host(host)
if mix_hosts[host] ~= nil then
mix_hosts[host] = mix_hosts[host] + 1;
module:log("debug", "Known MIX host has a new user");
else
module:log("debug", "Added %s as a new MIX host", host);
mix_hosts[host] = 1;
end
mix_pam:set("hosts", mix_hosts);
end
2021-02-20 19:55:07 +00:00
local function remove_mix_host(host)
if mix_hosts[host] ~= nil then
local count = mix_hosts[host];
if count == 1 then
mix_hosts[host] = nil;
module:log("debug", "Removing %s as a mix host", host);
else
mix_hosts[host] = count - 1;
module:log("debug", "Decrementing %s's user counter", host);
end
else
module:log("debug", "Attempt to remove unknown MIX host");
end
mix_pam:set("hosts", mix_hosts);
end
2021-02-20 19:55:07 +00:00
local function is_mix_host(host)
return mix_hosts[host] ~= nil;
end
2021-02-20 19:55:07 +00:00
local function is_mix_message(stanza)
return stanza:get_child("mix", "urn:xmpp:mix:core:1") ~= nil;
end
function module.load()
mix_hosts = mix_pam:get("hosts");
module:log("info", "Loaded known MIX hosts");
2021-02-20 19:55:07 +00:00
if mix_hosts == nil then
module:log("info", "No known MIX hosts loaded");
mix_hosts = {};
end
for host, _ in pairs(mix_hosts) do
module:log("debug", "Known host: %s", host);
end
end
local client_state_tracker = {}; -- [stanza ID] -> resource
2021-02-20 19:55:07 +00:00
local function add_state(id, resource)
client_state_tracker[id] = resource;
module:log("debug", "Adding a resource %s for id %s", resource, id);
end
2021-02-20 19:55:07 +00:00
local function has_state(id)
return client_state_tracker[id] ~= nil;
end
2021-02-20 19:55:07 +00:00
local function pop_state(id)
module:log("debug", "Popping a resource for stanza id %s", id);
if has_state(id) then
local resource = client_state_tracker[id];
client_state_tracker[id] = nil;
return resource;
end
return nil;
end
2021-02-20 19:55:07 +00:00
local function handle_client_join(event)
-- Client requests to join
module:log("debug", "client-join received");
local stanza, origin = event.stanza, event.origin;
2021-02-20 19:55:07 +00:00
local from = jid.bare(stanza.attr.from);
local client_join = stanza:get_child("client-join", mix_pam_xmlns);
if client_join.attr.channel == nil then
origin.send(st.error_reply(stanza, "cancel", "bad-request", "No channel specified"));
return true;
end
local join = client_join:get_child("join", "urn:xmpp:mix:core:1");
if join == nil then
origin.send(st.error_reply(stanza, "cancel", "bad-request", "No join stanza"));
return true;
end
-- Transform the client-join into a join
2021-02-20 19:55:07 +00:00
local join_iq = st.iq({
type = "set";
from = jid.bare(stanza.attr.from);
to = client_join.attr.channel;
id = stanza.attr.id, xmlns = "jabber:client"
});
join_iq:add_child(join);
2021-02-20 19:55:07 +00:00
add_state(stanza.attr.id, jid.resource(from));
module:send(join_iq);
return true;
end
2021-02-20 19:55:07 +00:00
local function handle_client_leave(event)
-- Client requests to leave
module:log("debug", "client-leave received");
local stanza, origin = event.stanza, event.origin;
2021-02-20 19:55:07 +00:00
local from = jid.bare(stanza.attr.from);
local client_leave = stanza:get_child("client-leave", mix_pam_xmlns);
if client_leave.attr.channel == nil then
origin.send(st.error_reply(stanza, "cancel", "bad-request", "No channel specified"));
return true;
end
local leave = client_leave:get_child("leave", "urn:xmpp:mix:core:1");
if leave == nil then
origin.send(st.error_reply(stanza, "cancel", "bad-request", "No leave stanza"));
return true;
end
-- Transform the client-join into a join
2021-02-20 19:55:07 +00:00
local leave_iq = st.iq({
type = "set";
from = jid.bare(stanza.attr.from);
to = client_leave.attr.channel;
id = stanza.attr.id
});
leave_iq:add_child(leave);
add_state(stanza.attr.id, jid.resource(from));
module:send(leave_iq);
return true;
end
2021-02-20 19:55:07 +00:00
module:hook("iq/self", function(event)
local stanza = event.stanza;
if #stanza.tags == 0 then return; end
if stanza:get_child("client-join", mix_pam_xmlns) ~= nil then
return handle_client_join(event);
elseif stanza:get_child("client-leave", mix_pam_xmlns) ~= nil then
return handle_client_leave(event);
end
end);
2021-02-20 19:55:07 +00:00
local function handle_mix_join(event)
-- The MIX server responded
module:log("debug", "Received MIX-JOIN result");
2021-02-20 19:55:07 +00:00
local stanza = event.stanza;
local spid = stanza:get_child("join", "urn:xmpp:mix:core:1").attr.id;
local channel_jid = spid.."#"..stanza.attr.from;
local origin = pop_state(stanza.attr.id);
local resource = origin.resource;
if resource == nil then
2021-02-20 19:55:07 +00:00
module:log("error", "Got a MIX join result for a not-requested id %s", stanza.attr.id);
module:log("error", "Maybe the server changed the stanza ID?");
return false;
end
2021-02-20 19:55:07 +00:00
local client_join = st.iq({
type = "result";
id = stanza.attr.id;
from = jid.bare(stanza.attr.to);
to = stanza.attr.to.."/"..resource
}):tag("client-join", { xmlns = mix_pam_xmlns, jid = channel_jid });
client_join:add_child(stanza:get_child("join", "urn:xmpp:mix:core:1"));
module:send(client_join);
2021-02-20 19:55:07 +00:00
-- TODO: Error handling?
rm_add_to_roster(origin, stanza.attr.from, {
subscription = "both",
groups = {},
2021-02-21 11:29:33 +00:00
mix_spid = spid,
});
2021-02-20 19:55:07 +00:00
rm_roster_push(jid.node(stanza.attr.to), module_host, stanza.attr.from);
2021-02-21 11:29:33 +00:00
add_mix_host(jid.bare(stanza.attr.from));
return true;
end
2021-02-20 19:55:07 +00:00
local function handle_mix_leave(event)
-- The MIX server responded
module:log("debug", "Received MIX-LEAVE result");
2021-02-20 19:55:07 +00:00
local stanza = event.stanza;
local origin = pop_state(stanza.attr.id);
local resource = origin.resource;
if resource == nil then
2021-02-20 19:55:07 +00:00
module:log("error", "Got a MIX leave result for a not-requested id %s", stanza.attr.id);
module:log("error", "Maybe the MIX server changed the stanza ID?");
return false;
end
2021-02-20 19:55:07 +00:00
local client_leave = st.iq({
type = "result";
id = stanza.attr.id;
from = jid.bare(stanza.attr.to);
to = stanza.attr.to.."/"..resource
}):tag("client-leave", { xmlns = mix_pam_xmlns });
client_leave:add_child(stanza:get_child("leave", "urn:xmpp:mix:core:1"));
2021-02-20 19:55:07 +00:00
module:send(client_leave);
-- Remove from roster
-- TODO: Error handling
rm_remove_from_roster(origin, jid.bare(stanza.attr.from));
2021-02-20 19:55:07 +00:00
rm_roster_push(jid.node(stanza.attr.to), module_host, jid.bare(stanza.attr.from));
2021-02-21 11:29:33 +00:00
remove_mix_host(jid.bare(stanza.attr.from));
return true;
end
2021-02-20 19:55:07 +00:00
module:hook("iq/bare", function(event)
-- We handle the MIX results here since IQ stanzas against bare JIDs
-- don't make a lot of sense. We first have to translate them into
-- full JIDs based on our stanza ID -> resource mapping
if event.stanza:get_child("leave", "urn:xmpp:mix:core:1") ~= nil then
return handle_mix_leave(event);
elseif event.stanza:get_child("join", "urn:xmpp:mix:core:1") ~= nil then
return handle_mix_join(event);
elseif has_state(event.stanza.attr.id) then
local tmp = st.clone(event.stanza);
tmp.attr.to = tmp.attr.to.."/"..pop_state(tmp.attr.id);
module:send(tmp);
return true;
end
end);
local user_resources = {}; -- [bare jid] -> array of bound resources
module:hook("resource-bind", function(event)
2021-02-20 19:55:07 +00:00
local session_jid, resource = jid.bare(event.session.full_jid), event.session.resource;
if user_resources[session_jid] ~= nil then
for _, r in pairs(user_resources) do
if r == resource then return; end
end
2021-02-20 19:55:07 +00:00
table.insert(user_resources[session_jid], resource);
else
2021-02-20 19:55:07 +00:00
user_resources[session_jid] = { resource };
end
2021-02-20 19:55:07 +00:00
module:log("debug", "Caught resource %s of %s", resource, session_jid);
end);
module:hook("resource-unbind", function(event)
2021-02-20 19:55:07 +00:00
local session_jid, resource = jid.bare(event.session.full_jid), event.session.resource;
for i, r in pairs(user_resources[session_jid]) do
if r == resource then
2021-02-20 19:55:07 +00:00
table.remove(user_resources[session_jid], i);
return;
end
end
2021-02-20 19:55:07 +00:00
module:log("debug", "Unbind of not recorded resource %s (%s)", resource, session_jid);
end);
local function child_with_attrs(stanza, childtag, xmlns, attrs)
-- Helper function: Like stanza:childtags, but returns only the first
-- child whose attributes are in attrs (and match the values in attrs).
-- Otherwise, nil is returned.
for _, child in ipairs(stanza:childtags(childtag, xmlns)) do
for attr_name, attr_value in pairs(attrs) do
if child.attrs[attr_name] ~= nil and child.attrs[attr_name] == attr_value then
return child;
else
-- We already now that this is not the child we're looking for
break;
end
end
end
return nil;
end
module:hook("roster-get", function(event)
-- NOTE: Currently this requires a patch to make mod_roster emit
-- the roster-get event
local annotate = event.stanza
:get_child("query", "jabber:iq:roster")
:get_child("annotate", mix_roster_xmlns);
if not annotate then return; end
2021-02-20 19:55:07 +00:00
local stanza = event.stanza;
module:log("debug", "Annotated roster request received");
-- User requested the roster with an <annotate/>
2021-02-21 11:29:33 +00:00
local query = event.reply:get_child("query", "jabber:iq:roster");
for contact, item in pairs(rm_load_roster(jid.bare(stanza.attr.from))) do
if item["mix_spid"] ~= nil then
local roster_item = child_with_attrs(query, "item", nil, { jid = contact });
if roster_item then
roster_item:tag("channel", { xmlns = mix_roster_xmlns, ["participant-id"] = item["mix_spid"] });
else
module:log("warn", "Could not find %s during annotated roster query!");
2021-02-21 11:29:33 +00:00
end
end
end
end);
module:hook("message/bare", function(event)
local stanza = event.stanza;
2021-02-20 19:55:07 +00:00
local jid_host = jid.host(stanza.attr.from);
if not is_mix_host(jid_host) then return; end
if not is_mix_message(stanza) then return; end
2021-02-20 19:55:07 +00:00
-- MIX-CORE says that if the message cannot be delivered, it should
-- just be dropped
if user_resources[stanza.attr.to] == nil then
module:log("debug", "Skipping %s: No resource bound", stanza.attr.to);
return true;
end
-- Per XEP we know that stanza.attr.to is the user's bare JID
for _, resource in pairs(user_resources[stanza.attr.to]) do
-- TODO: Only send to resources that advertise support for MIX (When MIX clients are available for testing)
local msg = st.clone(stanza);
msg.attr.to = stanza.attr.to.."/"..resource;
module:send(msg);
module:log("debug", "Sent message to %s", msg.attr.to);
end
return true;
end);