diff --git a/mod_mix_pam/mod_mix_pam.lua b/mod_mix_pam/mod_mix_pam.lua new file mode 100644 index 0000000..5b3084a --- /dev/null +++ b/mod_mix_pam/mod_mix_pam.lua @@ -0,0 +1,263 @@ +if module:get_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"); + +-- Persistent storage +local mix_pam = module:open_store("mix_pam", "keyval"); + +-- Runtime data +local mix_hosts = {}; + +-- Namespaceing +local mix_pam_xmlns = "urn:xmpp:mix:pam:2"; + +module:add_feature(mix_pam_xmlns); +-- NOTE: To show that we archive messages +-- module:add_feature(mix_pam_xmlns.."#archive"); + +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 +end + +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 +end + +function is_mix_host(host) + return mix_hosts[host] ~= nil; +end + +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"); + + if mix_hosts == nil then + module:log("info", "No known MIX hosts loaded"); + mix_hosts = {}; + return; + end + for _, host in pairs(mix_hosts) do + module:log("debug", "Known host: %s", host); + end +end + +local client_state_tracker = {}; -- [stanza ID] -> resource +function add_state(id, resource) + client_state_tracker[id] = resource; + module:log("debug", "Adding a resource %s for id %s", resource, id); +end +function has_state(id) + return client_state_tracker[id] ~= nil; +end +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 + +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); + +function handle_client_join(event) + -- Client requests to join + module:log("debug", "client-join received"); + local stanza, origin = event.stanza, event.origin; + local from, to = jid.bare(stanza.attr.from), jid.bare(stanza.attr.to); + + 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 + 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); + + add_state(stanza.attr.id, jid.resource(stanza.attr.from)); + + module:send(join_iq); + return true; +end + +function handle_client_leave(event) + -- Client requests to leave + module:log("debug", "client-leave received"); + local stanza, origin = event.stanza, event.origin; + local from, to = jid.bare(stanza.attr.from), jid.bare(stanza.attr.to); + + 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 + 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(stanza.attr.from)); + + module:send(leave_iq); + return true; +end + +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); + +function handle_mix_join(event) + -- The MIX server responded + -- TODO: Do stuff to the user's roster + module:log("debug", "Received MIX-JOIN result"); + add_mix_host(jid.host(event.stanza.attr.from)); + mix_pam:set("hosts", mix_hosts); + + local stanza = event.stanza; + local channel_jid = stanza:get_child("join", "urn:xmpp:mix:core:1").attr.id.."#"..stanza.attr.from; + local resource = pop_state(stanza.attr.id); + if resource == nil then + module:log("error", "Got a MIX join result for a not-requested id %s. Maybe the MIX server changed the stanza ID?", stanza.attr.id); + return false; + end + + 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); + return true; +end + +function handle_mix_leave(event) + -- The MIX server responded + -- TODO: Do stuff to the user's roster + module:log("debug", "Received MIX-LEAVE result"); + remove_mix_host(jid.host(event.stanza.attr.from)); + mix_pam:set("hosts", mix_hosts); + + local stanza = event.stanza; + local resource = pop_state(stanza.attr.id); + if resource == nil then + module:log("error", "Got a MIX leave result for a not-requested id %s. Maybe the MIX server changed the stanza ID?", stanza.attr.id); + return false; + end + + 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")); + module:send(client_leave); + return true; +end + +local user_resources = {}; -- [bare jid] -> array of bound resources + +module:hook("resource-bind", function(event) + local jid, resource = jid.bare(event.session.full_jid), event.session.resource; + if user_resources[jid] ~= nil then + for _, r in pairs(user_resources) do + if r == resource then return; end + end + + table.insert(user_resources[jid], resource); + else + user_resources[jid] = { resource }; + end + + module:log("debug", "Caught resource %s of %s", resource, jid); +end); + +module:hook("resource-unbind", function(event) + local jid, resource = jid.bare(event.session.full_jid), event.session.resource; + for i, r in pairs(user_resources[jid]) do + if r == resource then + table.remove(user_resources[jid], i); + return; + end + end + + module:log("debug", "Unbind of not recorded resource %s (%s)", resource, jid); +end); + +module:hook("message/bare", function(event) + local stanza = event.stanza; + local host = jid.host(stanza.attr.from); + if not is_mix_host(host) then return; end + if not is_mix_message(stanza) then return; end + + -- 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);