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
parents cf9cd666ba00
children 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)