Software / code / prosody-modules
Comparison
mod_delegation/mod_delegation.lua @ 2752:d0e75bf21d30
mod_delegation: added disco#items support
disco#items are forwarded to managing entity when suitable. This feature is not yet in XEP-0355, but it should be added soon.
"http://jabber.org/protocol/disco#items:*" is used as a pseudo-namespace to activate this delegation.
Also changed spaces to tabs to follow Prosody coding style.
| author | Goffi <goffi@goffi.org> |
|---|---|
| date | Sun, 27 Aug 2017 20:46:04 +0200 |
| parent | 2069:cf9cd666ba00 |
| child | 2758:82109d8eca41 |
comparison
equal
deleted
inserted
replaced
| 2710:956b75b0e9d9 | 2752:d0e75bf21d30 |
|---|---|
| 13 local st = require("util.stanza") | 13 local st = require("util.stanza") |
| 14 local set = require("util.set") | 14 local set = require("util.set") |
| 15 | 15 |
| 16 local delegation_session = module:shared("/*/delegation/session") | 16 local delegation_session = module:shared("/*/delegation/session") |
| 17 | 17 |
| 18 -- FIXME: temporarily needed for disco_items_hook, to be removed when clean implementation is done | |
| 19 local is_contact_subscribed = require "core.rostermanager".is_contact_subscribed; | |
| 20 local jid_split = require "util.jid".split; | |
| 21 local jid_bare = require "util.jid".bare; | |
| 22 | |
| 18 if delegation_session.connected_cb == nil then | 23 if delegation_session.connected_cb == nil then |
| 19 -- set used to have connected event listeners | 24 -- set used to have connected event listeners |
| 20 -- which allow a host to react on events from | 25 -- which allow a host to react on events from |
| 21 -- other hosts | 26 -- other hosts |
| 22 delegation_session.connected_cb = set.new() | 27 delegation_session.connected_cb = set.new() |
| 23 end | 28 end |
| 24 local connected_cb = delegation_session.connected_cb | 29 local connected_cb = delegation_session.connected_cb |
| 25 | 30 |
| 26 local _DELEGATION_NS = 'urn:xmpp:delegation:1' | 31 local _DELEGATION_NS = 'urn:xmpp:delegation:1' |
| 27 local _FORWARDED_NS = 'urn:xmpp:forward:0' | 32 local _FORWARDED_NS = 'urn:xmpp:forward:0' |
| 28 local _DISCO_NS = 'http://jabber.org/protocol/disco#info' | 33 local _DISCO_INFO_NS = 'http://jabber.org/protocol/disco#info' |
| 34 local _DISCO_ITEMS_NS = 'http://jabber.org/protocol/disco#items' | |
| 29 local _DATA_NS = 'jabber:x:data' | 35 local _DATA_NS = 'jabber:x:data' |
| 30 | 36 |
| 31 local _MAIN_SEP = '::' | 37 local _MAIN_SEP = '::' |
| 32 local _BARE_SEP = ':bare:' | 38 local _BARE_SEP = ':bare:' |
| 39 local _REMAINING = ':*' | |
| 33 local _MAIN_PREFIX = _DELEGATION_NS.._MAIN_SEP | 40 local _MAIN_PREFIX = _DELEGATION_NS.._MAIN_SEP |
| 34 local _BARE_PREFIX = _DELEGATION_NS.._BARE_SEP | 41 local _BARE_PREFIX = _DELEGATION_NS.._BARE_SEP |
| 42 local _DISCO_REMAINING = _DISCO_ITEMS_NS.._REMAINING | |
| 35 local _PREFIXES = {_MAIN_PREFIX, _BARE_PREFIX} | 43 local _PREFIXES = {_MAIN_PREFIX, _BARE_PREFIX} |
| 36 | 44 |
| 37 local disco_nest | 45 local disco_nest |
| 38 | 46 |
| 39 module:log("debug", "Loading namespace delegation module "); | 47 module:log("debug", "Loading namespace delegation module ") |
| 40 | 48 |
| 41 --> Configuration management <-- | 49 --> Configuration management <-- |
| 42 | 50 |
| 43 local ns_delegations = module:get_option("delegations", {}) | 51 local ns_delegations = module:get_option("delegations", {}) |
| 44 | 52 |
| 93 -- if the namespace has already a connected entity, ignore the new one | 101 -- if the namespace has already a connected entity, ignore the new one |
| 94 local function set_config(jid_) | 102 local function set_config(jid_) |
| 95 for namespace, ns_data in pairs(jid2ns[jid_]) do | 103 for namespace, ns_data in pairs(jid2ns[jid_]) do |
| 96 if ns_data.connected == nil then | 104 if ns_data.connected == nil then |
| 97 ns_data.connected = entity_jid | 105 ns_data.connected = entity_jid |
| 98 disco_nest(namespace, entity_jid) | 106 -- disco remaining is a special namespace |
| 107 -- there is no disco nesting for it | |
| 108 if namespace ~= _DISCO_REMAINING then | |
| 109 disco_nest(namespace, entity_jid) | |
| 110 end | |
| 99 end | 111 end |
| 100 end | 112 end |
| 101 end | 113 end |
| 102 local bare_jid = jid.bare(entity_jid) | 114 local bare_jid = jid.bare(entity_jid) |
| 103 set_config(bare_jid) | 115 set_config(bare_jid) |
| 142 callback(event) | 154 callback(event) |
| 143 end | 155 end |
| 144 end | 156 end |
| 145 | 157 |
| 146 if module:get_host_type() ~= "component" then | 158 if module:get_host_type() ~= "component" then |
| 147 connected_cb:add(on_component_connected) | 159 connected_cb:add(on_component_connected) |
| 148 end | 160 end |
| 149 module:hook('component-authenticated', on_component_auth) | 161 module:hook('component-authenticated', on_component_auth) |
| 150 module:hook('presence/initial', on_presence) | 162 module:hook('presence/initial', on_presence) |
| 151 | 163 |
| 152 | 164 |
| 184 return true | 196 return true |
| 185 end | 197 end |
| 186 | 198 |
| 187 local iq = forwarded.tags[1] | 199 local iq = forwarded.tags[1] |
| 188 if #forwarded ~= 1 or iq.name ~= "iq" or | 200 if #forwarded ~= 1 or iq.name ~= "iq" or |
| 189 iq.attr.xmlns ~= 'jabber:client' or | 201 iq.attr.xmlns ~= 'jabber:client' or |
| 190 (iq.attr.type =='result' and #iq > 1) or | 202 (iq.attr.type =='result' and #iq > 1) or |
| 191 (iq.attr.type == 'error' and #iq > 2) then | 203 (iq.attr.type == 'error' and #iq > 2) then |
| 192 module:log("warn", "ignoring invalid iq result from managing entity %s", stanza.attr.from) | 204 module:log("warn", "ignoring invalid iq result from managing entity %s", stanza.attr.from) |
| 193 stanza_cache[stanza.attr.from][stanza.attr.id] = nil | 205 stanza_cache[stanza.attr.from][stanza.attr.id] = nil |
| 194 return true | 206 return true |
| 195 end | 207 end |
| 196 | 208 |
| 197 iq.attr.xmlns = nil | 209 iq.attr.xmlns = nil |
| 198 | 210 |
| 199 local original = stanza_cache[stanza.attr.from][stanza.attr.id] | 211 local original = stanza_cache[stanza.attr.from][stanza.attr.id] |
| 200 stanza_cache[stanza.attr.from][stanza.attr.id] = nil | 212 stanza_cache[stanza.attr.from][stanza.attr.id] = nil |
| 201 -- we get namespace from original and not iq | 213 -- we get namespace from original and not iq |
| 202 -- because the namespace can be lacking in case of error | 214 -- because the namespace can be lacking in case of error |
| 203 local namespace = original.tags[1].attr.xmlns | 215 local namespace = original.tags[1].attr.xmlns |
| 216 | |
| 217 -- small hack for disco remaining feat | |
| 218 if namespace == _DISCO_ITEMS_NS then | |
| 219 namespace = _DISCO_REMAINING | |
| 220 end | |
| 221 | |
| 204 local ns_data = ns_delegations[namespace] | 222 local ns_data = ns_delegations[namespace] |
| 205 | 223 |
| 206 if stanza.attr.from ~= ns_data.connected or (iq.attr.type ~= "result" and iq.attr.type ~= "error") or | 224 if stanza.attr.from ~= ns_data.connected or (iq.attr.type ~= "result" and iq.attr.type ~= "error") or |
| 207 iq.attr.id ~= original.attr.id or iq.attr.to ~= original.attr.from then | 225 iq.attr.id ~= original.attr.id or iq.attr.to ~= original.attr.from then |
| 208 module:log("warn", "ignoring forbidden iq result from managing entity %s, please check that the component is no trying to do something bad (stanza: %s)", stanza.attr.from, tostring(stanza)) | 226 module:log("warn", "ignoring forbidden iq result from managing entity %s, please check that the component is no trying to do something bad (stanza: %s)", stanza.attr.from, tostring(stanza)) |
| 211 end | 229 end |
| 212 | 230 |
| 213 -- at this point eveything is checked, | 231 -- at this point eveything is checked, |
| 214 -- and we (hopefully) can send the the result safely | 232 -- and we (hopefully) can send the the result safely |
| 215 module:send(iq) | 233 module:send(iq) |
| 216 return true | 234 return true |
| 217 end | 235 end |
| 218 | 236 |
| 219 function managing_ent_error(event) | 237 function managing_ent_error(event) |
| 220 local stanza = event.stanza | 238 local stanza = event.stanza |
| 221 if stanza.attr.to ~= module.host then | 239 if stanza.attr.to ~= module.host then |
| 226 module:unhook("iq-error/host/"..stanza.attr.id, managing_ent_error) | 244 module:unhook("iq-error/host/"..stanza.attr.id, managing_ent_error) |
| 227 local original = stanza_cache[stanza.attr.from][stanza.attr.id] | 245 local original = stanza_cache[stanza.attr.from][stanza.attr.id] |
| 228 stanza_cache[stanza.attr.from][stanza.attr.id] = nil | 246 stanza_cache[stanza.attr.from][stanza.attr.id] = nil |
| 229 module:log("warn", "Got an error after forwarding stanza to "..stanza.attr.from) | 247 module:log("warn", "Got an error after forwarding stanza to "..stanza.attr.from) |
| 230 module:send(st.error_reply(original, 'cancel', 'service-unavailable')) | 248 module:send(st.error_reply(original, 'cancel', 'service-unavailable')) |
| 231 return true | 249 return true |
| 232 end | 250 end |
| 233 | 251 |
| 234 local function forward_iq(stanza, ns_data) | 252 local function forward_iq(stanza, ns_data) |
| 235 local to_jid = ns_data.connected | 253 local to_jid = ns_data.connected |
| 236 stanza.attr.xmlns = 'jabber:client' | 254 stanza.attr.xmlns = 'jabber:client' |
| 268 for _, attribute in pairs(ns_data.filtering) do | 286 for _, attribute in pairs(ns_data.filtering) do |
| 269 -- if any filtered attribute if not present, | 287 -- if any filtered attribute if not present, |
| 270 -- we must continue the normal bahaviour | 288 -- we must continue the normal bahaviour |
| 271 if not first_child.attr[attribute] then | 289 if not first_child.attr[attribute] then |
| 272 -- Filtered attribute is not present, we do normal workflow | 290 -- Filtered attribute is not present, we do normal workflow |
| 273 return; | 291 return |
| 274 end | 292 end |
| 275 end | 293 end |
| 276 end | 294 end |
| 277 if not ns_data.connected then | 295 if not ns_data.connected then |
| 278 module:log("warn", "No connected entity to manage "..namespace) | 296 module:log("warn", "No connected entity to manage "..namespace) |
| 348 end | 366 end |
| 349 | 367 |
| 350 local function extension_added(event) | 368 local function extension_added(event) |
| 351 local source, stanza = event.source, event.item | 369 local source, stanza = event.source, event.item |
| 352 local form_type = find_form_type(stanza) | 370 local form_type = find_form_type(stanza) |
| 353 if not form_type then return; end | 371 if not form_type then return end |
| 354 | 372 |
| 355 for namespace, _ in pairs(ns_delegations) do | 373 for namespace, _ in pairs(ns_delegations) do |
| 356 if source ~= module and string.sub(form_type, 1, #namespace) == namespace then | 374 if source ~= module and string.sub(form_type, 1, #namespace) == namespace then |
| 357 module:log("debug", "Removing extension which is delegated: %s", tostring(stanza)) | 375 module:log("debug", "Removing extension which is delegated: %s", tostring(stanza)) |
| 358 source:remove_item("extension", stanza) | 376 source:remove_item("extension", stanza) |
| 359 end | 377 end |
| 360 end | 378 end |
| 361 end | 379 end |
| 362 | 380 |
| 363 -- for disco nesting (see § 7.2) we need to remove internal features | 381 -- for disco nesting (see § 7.2) we need to remove internal features |
| 364 -- we use handle_items as it allow to remove already added features | 382 -- we use handle_items as it allows to remove already added features |
| 365 -- and catch the ones which can come later | 383 -- and catch the ones which can come later |
| 366 module:handle_items("feature", feature_added, function(_) end) | 384 module:handle_items("feature", feature_added, function(_) end) |
| 367 module:handle_items("identity", identity_added, function(_) end, false) | 385 module:handle_items("identity", identity_added, function(_) end, false) |
| 368 module:handle_items("extension", extension_added, function(_) end) | 386 module:handle_items("extension", extension_added, function(_) end) |
| 369 | 387 |
| 383 module:log("warn", 'Stanza result has "to" attribute not addressed to current host, id conflict ?') | 401 module:log("warn", 'Stanza result has "to" attribute not addressed to current host, id conflict ?') |
| 384 return | 402 return |
| 385 end | 403 end |
| 386 module:unhook("iq-result/host/"..stanza.attr.id, disco_result) | 404 module:unhook("iq-result/host/"..stanza.attr.id, disco_result) |
| 387 module:unhook("iq-error/host/"..stanza.attr.id, disco_error) | 405 module:unhook("iq-error/host/"..stanza.attr.id, disco_error) |
| 388 local query = stanza:get_child("query", _DISCO_NS) | 406 local query = stanza:get_child("query", _DISCO_INFO_NS) |
| 389 if not query or not query.attr.node then | 407 if not query or not query.attr.node then |
| 390 session.send(st.error_reply(stanza, 'modify', 'not-acceptable')) | 408 session.send(st.error_reply(stanza, 'modify', 'not-acceptable')) |
| 391 return true | 409 return true |
| 392 end | 410 end |
| 393 | 411 |
| 456 | 474 |
| 457 for _, prefix in ipairs(_PREFIXES) do | 475 for _, prefix in ipairs(_PREFIXES) do |
| 458 local node = prefix..namespace | 476 local node = prefix..namespace |
| 459 | 477 |
| 460 local iq = st.iq({from=module.host, to=entity_jid, type='get'}) | 478 local iq = st.iq({from=module.host, to=entity_jid, type='get'}) |
| 461 :tag('query', {xmlns=_DISCO_NS, node=node}) | 479 :tag('query', {xmlns=_DISCO_INFO_NS, node=node}) |
| 462 | 480 |
| 463 local iq_id = iq.attr.id | 481 local iq_id = iq.attr.id |
| 464 | 482 |
| 465 module:hook("iq-result/host/"..iq_id, disco_result) | 483 module:hook("iq-result/host/"..iq_id, disco_result) |
| 466 module:hook("iq-error/host/"..iq_id, disco_error) | 484 module:hook("iq-error/host/"..iq_id, disco_error) |
| 467 module:send(iq) | 485 module:send(iq) |
| 468 end | 486 end |
| 469 end | 487 end |
| 470 | 488 |
| 471 -- disco to bare jids special case | 489 -- disco to bare jids special cases |
| 472 | 490 |
| 473 module:hook("account-disco-info", function(event) | 491 -- disco#info |
| 492 | |
| 493 local function disco_hook(event) | |
| 474 -- this event is called when a disco info request is done on a bare jid | 494 -- this event is called when a disco info request is done on a bare jid |
| 475 -- we get the final reply and filter delegated features/identities/extensions | 495 -- we get the final reply and filter delegated features/identities/extensions |
| 476 local reply = event.reply; | 496 local reply = event.reply |
| 477 reply.tags[1]:maptags(function(child) | 497 reply.tags[1]:maptags(function(child) |
| 478 if child.name == 'feature' then | 498 if child.name == 'feature' then |
| 479 local feature_ns = child.attr.var | 499 local feature_ns = child.attr.var |
| 480 for namespace, _ in pairs(ns_delegations) do | 500 for namespace, _ in pairs(ns_delegations) do |
| 481 if string.sub(feature_ns, 1, #namespace) == namespace then | 501 if string.sub(feature_ns, 1, #namespace) == namespace then |
| 516 end | 536 end |
| 517 for _, stanza in ipairs(bare_extensions) do | 537 for _, stanza in ipairs(bare_extensions) do |
| 518 reply:add_child(stanza) | 538 reply:add_child(stanza) |
| 519 end | 539 end |
| 520 | 540 |
| 521 end, -2^32); | 541 end |
| 542 | |
| 543 -- disco#items | |
| 544 | |
| 545 local function disco_items_node_hook(event) | |
| 546 -- check if node is not handled by server | |
| 547 -- and forward the disco request to suitable entity | |
| 548 if not event.exists then | |
| 549 -- this node is not handled by the server | |
| 550 local ns_data = ns_delegations[_DISCO_REMAINING] | |
| 551 if ns_data ~= nil then | |
| 552 -- remaining delegation is requested, we forward | |
| 553 forward_iq(event.stanza, ns_data) | |
| 554 -- and stop normal event handling | |
| 555 return true | |
| 556 end | |
| 557 end | |
| 558 end | |
| 559 module:hook("account-disco-items-node", disco_items_node_hook, -2^32) | |
| 560 | |
| 561 local function disco_items_hook(event) | |
| 562 -- FIXME: we forward all bare-jid disco-items requests (without node) which will replace any Prosody reply | |
| 563 -- for now it's OK because Prosody is not returning anything on request on bare jid | |
| 564 -- but to be properly done, any Prosody reply should be kept and managing entities items should be added (merged) to it. | |
| 565 -- account-disco-items can't be cancelled (return value of hooks are not checked in mod_disco), so corountine needs | |
| 566 -- to be used with util.async (to get the IQ result, merge items then return from the event) | |
| 567 local origin, stanza = event.origin, event.stanza; | |
| 568 if stanza.attr.type ~= "get" then return; end | |
| 569 local node = stanza.tags[1].attr.node; | |
| 570 local username = jid_split(stanza.attr.to) or origin.username; | |
| 571 if not stanza.attr.to or is_contact_subscribed(username, module.host, jid_bare(stanza.attr.from)) then | |
| 572 if node == nil or node == "" then | |
| 573 local ns_data = ns_delegations[_DISCO_REMAINING] | |
| 574 if ns_data ~= nil then | |
| 575 forward_iq(event.stanza, ns_data) | |
| 576 return true | |
| 577 end | |
| 578 end | |
| 579 end | |
| 580 end | |
| 581 module:hook("iq/bare/http://jabber.org/protocol/disco#items:query", disco_items_hook, 100) | |
| 582 | |
| 583 local function disco_items_raw_hook(event) | |
| 584 -- this method is called when account-disco-items-* event are not called | |
| 585 -- notably when a disco-item is done by an unsubscibed entity | |
| 586 -- (i.e. an entity doing a disco#item on an entity without having | |
| 587 -- presence subscription) | |
| 588 -- we forward the request to managing entity | |
| 589 -- it's the responsability of the managing entity to filter the items | |
| 590 local ns_data = ns_delegations[_DISCO_REMAINING] | |
| 591 if ns_data ~= nil then | |
| 592 forward_iq(event.stanza, ns_data) | |
| 593 return true | |
| 594 end | |
| 595 end | |
| 596 module:hook("iq-get/bare/http://jabber.org/protocol/disco#items:query", disco_items_raw_hook, -2^32) |