comparison mod_delegation/mod_delegation.lua @ 1725:d85d5b0bf977

Merge with Goffi
author Kim Alvefur <zash@zash.se>
date Thu, 07 May 2015 23:39:54 +0200
parents 2440a75e868f
children 7bfc23b2c038
comparison
equal deleted inserted replaced
1706:e4867211cddb 1725:d85d5b0bf977
1 -- XEP-0355 (Namespace Delegation)
2 -- Copyright (C) 2015 Jérôme Poisson
3 --
4 -- This module is MIT/X11 licensed. Please see the
5 -- COPYING file in the source package for more information.
6
7 -- This module manage namespace delegation, a way to delegate server features
8 -- to an external entity/component. Only the admin mode is implemented so far
9
10 -- TODO: client mode
11
12 local jid = require("util/jid")
13 local st = require("util/stanza")
14 local set = require("util/set")
15
16 local delegation_session = module:shared("/*/delegation/session")
17
18 if delegation_session.connected_cb == nil then
19 -- set used to have connected event listeners
20 -- which allow a host to react on events from
21 -- other hosts
22 delegation_session.connected_cb = set.new()
23 end
24 local connected_cb = delegation_session.connected_cb
25
26 local _DELEGATION_NS = 'urn:xmpp:delegation:1'
27 local _FORWARDED_NS = 'urn:xmpp:forward:0'
28 local _DISCO_NS = 'http://jabber.org/protocol/disco#info'
29 local _DATA_NS = 'jabber:x:data'
30
31 local _MAIN_SEP = '::'
32 local _BARE_SEP = ':bare:'
33 local _MAIN_PREFIX = _DELEGATION_NS.._MAIN_SEP
34 local _BARE_PREFIX = _DELEGATION_NS.._BARE_SEP
35 local _PREFIXES = {_MAIN_PREFIX, _BARE_PREFIX}
36
37 local disco_nest
38
39 module:log("debug", "Loading namespace delegation module ");
40
41 --> Configuration management <--
42
43 local ns_delegations = module:get_option("delegations", {})
44
45 local jid2ns = {}
46 for namespace, ns_data in pairs(ns_delegations) do
47 -- "connected" contain the full jid of connected managing entity
48 ns_data.connected = nil
49 if ns_data.jid then
50 if jid2ns[ns_data.jid] == nil then
51 jid2ns[ns_data.jid] = {}
52 end
53 jid2ns[ns_data.jid][namespace] = ns_data
54 module:log("debug", "Namespace %s is delegated%s to %s", namespace, ns_data.filtering and " (with filtering)" or "", ns_data.jid)
55 else
56 module:log("warn", "Ignoring delegation for %s: no jid specified", tostring(namespace))
57 ns_delegations[namespace] = nil
58 end
59 end
60
61
62 local function advertise_delegations(session, to_jid)
63 -- send <message/> stanza to advertise delegations
64 -- as expained in § 4.2
65 local message = st.message({from=module.host, to=to_jid})
66 :tag("delegation", {xmlns=_DELEGATION_NS})
67
68 -- we need to check if a delegation is granted because the configuration
69 -- can be complicated if some delegations are granted to bare jid
70 -- and other to full jids, and several resources are connected.
71 local have_delegation = false
72
73 for namespace, ns_data in pairs(jid2ns[to_jid]) do
74 if ns_data.connected == to_jid then
75 have_delegation = true
76 message:tag("delegated", {namespace=namespace})
77 if type(ns_data.filtering) == "table" then
78 for _, attribute in pairs(ns_data.filtering) do
79 message:tag("attribute", {name=attribute}):up()
80 end
81 message:up()
82 end
83 end
84 end
85
86 if have_delegation then
87 session.send(message)
88 end
89 end
90
91 local function set_connected(entity_jid)
92 -- set the "connected" key for all namespace managed by entity_jid
93 -- if the namespace has already a connected entity, ignore the new one
94 local function set_config(jid_)
95 for namespace, ns_data in pairs(jid2ns[jid_]) do
96 if ns_data.connected == nil then
97 ns_data.connected = entity_jid
98 disco_nest(namespace, entity_jid)
99 end
100 end
101 end
102 local bare_jid = jid.bare(entity_jid)
103 set_config(bare_jid)
104 -- We can have a bare jid of a full jid specified in configuration
105 -- so we try our luck with both (first connected resource will
106 -- manage the namespaces in case of bare jid)
107 if bare_jid ~= entity_jid then
108 set_config(entity_jid)
109 jid2ns[entity_jid] = jid2ns[bare_jid]
110 end
111 end
112
113 local function on_presence(event)
114 local session = event.origin
115 local bare_jid = jid.bare(session.full_jid)
116
117 if jid2ns[bare_jid] or jid2ns[session.full_jid] then
118 set_connected(session.full_jid)
119 advertise_delegations(session, session.full_jid)
120 end
121 end
122
123 local function on_component_connected(event)
124 -- method called by the module loaded by the component
125 -- /!\ the event come from the component host,
126 -- not from the host of this module
127 local session = event.session
128 local bare_jid = jid.join(session.username, session.host)
129
130 local jid_delegations = jid2ns[bare_jid]
131 if jid_delegations ~= nil then
132 set_connected(bare_jid)
133 advertise_delegations(session, bare_jid)
134 end
135 end
136
137 local function on_component_auth(event)
138 -- react to component-authenticated event from this host
139 -- and call the on_connected methods from all other hosts
140 -- needed for the component to get delegations advertising
141 for callback in connected_cb:items() do
142 callback(event)
143 end
144 end
145
146 connected_cb:add(on_component_connected)
147 module:hook('component-authenticated', on_component_auth)
148 module:hook('presence/initial', on_presence)
149
150
151 --> delegated namespaces hook <--
152
153 local managing_ent_error
154 local stanza_cache = {} -- we cache original stanza to build reply
155
156 local function managing_ent_result(event)
157 -- this function manage iq results from the managing entity
158 -- it do a couple of security check before sending the
159 -- result to the managed entity
160 local stanza = event.stanza
161 if stanza.attr.to ~= module.host then
162 module:log("warn", 'forwarded stanza result has "to" attribute not addressed to current host, id conflict ?')
163 return
164 end
165 module:unhook("iq-result/host/"..stanza.attr.id, managing_ent_result)
166 module:unhook("iq-error/host/"..stanza.attr.id, managing_ent_error)
167
168 -- lot of checks to do...
169 local delegation = stanza.tags[1]
170 if #stanza ~= 1 or delegation.name ~= "delegation" or
171 delegation.attr.xmlns ~= _DELEGATION_NS then
172 module:log("warn", "ignoring invalid iq result from managing entity %s", stanza.attr.from)
173 stanza_cache[stanza.attr.from][stanza.attr.id] = nil
174 return true
175 end
176
177 local forwarded = delegation.tags[1]
178 if #delegation ~= 1 or forwarded.name ~= "forwarded" or
179 forwarded.attr.xmlns ~= _FORWARDED_NS then
180 module:log("warn", "ignoring invalid iq result from managing entity %s", stanza.attr.from)
181 stanza_cache[stanza.attr.from][stanza.attr.id] = nil
182 return true
183 end
184
185 local iq = forwarded.tags[1]
186 if #forwarded ~= 1 or iq.name ~= "iq" or
187 iq.attr.xmlns ~= 'jabber:client' or
188 (iq.attr.type =='result' and #iq ~= 1) or
189 (iq.attr.type == 'error' and #iq > 2) then
190 module:log("warn", "ignoring invalid iq result from managing entity %s", stanza.attr.from)
191 stanza_cache[stanza.attr.from][stanza.attr.id] = nil
192 return true
193 end
194
195 iq.attr.xmlns = nil
196
197 local original = stanza_cache[stanza.attr.from][stanza.attr.id]
198 stanza_cache[stanza.attr.from][stanza.attr.id] = nil
199 -- we get namespace from original and not iq
200 -- because the namespace can be lacking in case of error
201 local namespace = original.tags[1].attr.xmlns
202 local ns_data = ns_delegations[namespace]
203
204 if stanza.attr.from ~= ns_data.connected or (iq.attr.type ~= "result" and iq.attr.type ~= "error") or
205 iq.attr.id ~= original.attr.id or iq.attr.to ~= original.attr.from then
206 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))
207 module:send(st.error_reply(original, 'cancel', 'service-unavailable'))
208 return true
209 end
210
211 -- at this point eveything is checked,
212 -- and we (hopefully) can send the the result safely
213 module:send(iq)
214 return true
215 end
216
217 function managing_ent_error(event)
218 local stanza = event.stanza
219 if stanza.attr.to ~= module.host then
220 module:log("warn", 'Stanza result has "to" attribute not addressed to current host, id conflict ?')
221 return
222 end
223 module:unhook("iq-result/host/"..stanza.attr.id, managing_ent_result)
224 module:unhook("iq-error/host/"..stanza.attr.id, managing_ent_error)
225 local original = stanza_cache[stanza.attr.from][stanza.attr.id]
226 stanza_cache[stanza.attr.from][stanza.attr.id] = nil
227 module:log("warn", "Got an error after forwarding stanza to "..stanza.attr.from)
228 module:send(st.error_reply(original, 'cancel', 'service-unavailable'))
229 return true
230 end
231
232 local function forward_iq(stanza, ns_data)
233 local to_jid = ns_data.connected
234 stanza.attr.xmlns = 'jabber:client'
235 local iq_stanza = st.iq({ from=module.host, to=to_jid, type="set" })
236 :tag("delegation", { xmlns=_DELEGATION_NS })
237 :tag("forwarded", { xmlns=_FORWARDED_NS })
238 :add_child(stanza)
239 local iq_id = iq_stanza.attr.id
240 -- we save the original stanza to check the managing entity result
241 if not stanza_cache[to_jid] then stanza_cache[to_jid] = {} end
242 stanza_cache[to_jid][iq_id] = stanza
243 module:hook("iq-result/host/"..iq_id, managing_ent_result)
244 module:hook("iq-error/host/"..iq_id, managing_ent_error)
245 module:log("debug", "stanza forwarded")
246 module:send(iq_stanza)
247 end
248
249 local function iq_hook(event)
250 -- general hook for all the iq which forward delegated ones
251 -- and continue normal behaviour else. If a namespace is
252 -- delegated but managing entity is offline, a service-unavailable
253 -- error will be sent, as requested by the XEP
254 local session, stanza = event.origin, event.stanza
255 if #stanza == 1 and stanza.attr.type == 'get' or stanza.attr.type == 'set' then
256 local namespace = stanza.tags[1].attr.xmlns
257 local ns_data = ns_delegations[namespace]
258
259 if ns_data then
260 if stanza.attr.from == ns_data.connected then
261 -- we don't forward stanzas from managing entity itself
262 return
263 end
264 if ns_data.filtering then
265 local first_child = stanza.tags[1]
266 for _, attribute in ns_data.filtering do
267 -- if any filtered attribute if not present,
268 -- we must continue the normal bahaviour
269 if not first_child.attr[attribute] then
270 -- Filtered attribute is not present, we do normal workflow
271 return;
272 end
273 end
274 end
275 if not ns_data.connected then
276 module:log("warn", "No connected entity to manage "..namespace)
277 session.send(st.error_reply(stanza, 'cancel', 'service-unavailable'))
278 else
279 forward_iq(stanza, ns_data)
280 end
281 return true
282 else
283 -- we have no delegation, we continue normal behaviour
284 return
285 end
286 end
287 end
288
289 module:hook("iq/self", iq_hook, 2^32)
290 module:hook("iq/bare", iq_hook, 2^32)
291 module:hook("iq/host", iq_hook, 2^32)
292
293
294 --> discovery nesting <--
295
296 -- disabling internal features/identities
297
298 local function find_form_type(stanza)
299 local form_type = nil
300 for field in stanza.childtags('field', 'jabber:x:data') do
301 if field.attr.var=='FORM_TYPE' and field.attr.type=='hidden' then
302 local value = field:get_child('value')
303 if not value then
304 module:log("warn", "No value found in FORM_TYPE field: "..tostring(stanza))
305 else
306 form_type=value.get_text()
307 end
308 end
309 end
310 return form_type
311 end
312
313 -- modules whose features/identities are managed by delegation
314 local disabled_modules = set.new()
315 local disabled_identities = set.new()
316
317 local function identity_added(event)
318 local source = event.source
319 if disabled_modules:contains(source) then
320 local item = event.item
321 local category, type_, name = item.category, item.type, item.name
322 module:log("debug", "Removing (%s/%s%s) identity because of delegation", category, type_, name and "/"..name or "")
323 disabled_identities:add(item)
324 source:remove_item("identity", item)
325 end
326 end
327
328 local function feature_added(event)
329 local source, item = event.source, event.item
330 for namespace, _ in pairs(ns_delegations) do
331 if source ~= module and string.sub(item, 1, #namespace) == namespace then
332 module:log("debug", "Removing %s feature which is delegated", item)
333 source:remove_item("feature", item)
334 disabled_modules:add(source)
335 if source.items and source.items.identity then
336 -- we remove all identities added by the source module
337 -- that can cause issues if the module manages several features/identities
338 -- but this case is probably rare (or doesn't happen at all)
339 -- FIXME: any better way ?
340 for _, identity in pairs(source.items.identity) do
341 identity_added({source=source, item=identity})
342 end
343 end
344 end
345 end
346 end
347
348 local function extension_added(event)
349 local source, stanza = event.source, event.item
350 local form_type = find_form_type(stanza)
351 if not form_type then return; end
352
353 for namespace, _ in pairs(ns_delegations) do
354 if source ~= module and string.sub(form_type, 1, #namespace) == namespace then
355 module:log("debug", "Removing extension which is delegated: %s", tostring(stanza))
356 source:remove_item("extension", stanza)
357 end
358 end
359 end
360
361 -- for disco nesting (see § 7.2) we need to remove internal features
362 -- we use handle_items as it allow to remove already added features
363 -- and catch the ones which can come later
364 module:handle_items("feature", feature_added, function(_) end)
365 module:handle_items("identity", identity_added, function(_) end, false)
366 module:handle_items("extension", extension_added, function(_) end)
367
368
369 -- managing entity features/identities collection
370
371 local disco_error
372 local bare_features = set.new()
373 local bare_identities = {}
374 local bare_extensions = {}
375
376 local function disco_result(event)
377 -- parse result from disco nesting request
378 -- and fill module features/identities and bare_features/bare_identities accordingly
379 local session, stanza = event.origin, event.stanza
380 if stanza.attr.to ~= module.host then
381 module:log("warn", 'Stanza result has "to" attribute not addressed to current host, id conflict ?')
382 return
383 end
384 module:unhook("iq-result/host/"..stanza.attr.id, disco_result)
385 module:unhook("iq-error/host/"..stanza.attr.id, disco_error)
386 local query = stanza:get_child("query", _DISCO_NS)
387 if not query or not query.attr.node then
388 session.send(st.error_reply(stanza, 'modify', 'not-acceptable'))
389 return true
390 end
391
392 local node = query.attr.node
393 local main
394
395 if string.sub(node, 1, #_MAIN_PREFIX) == _MAIN_PREFIX then
396 main=true
397 elseif string.sub(node, 1, #_BARE_PREFIX) == _BARE_PREFIX then
398 main=false
399 else
400 module:log("warn", "Unexpected node: "..node)
401 session.send(st.error_reply(stanza, 'modify', 'not-acceptable'))
402 return true
403 end
404
405 for feature in query:childtags("feature") do
406 local namespace = feature.attr.var
407 if main then
408 module:add_feature(namespace)
409 else
410 bare_features:add(namespace)
411 end
412 end
413 for identity in query:childtags("identity") do
414 local category, type_, name = identity.attr.category, identity.attr.type, identity.attr.name
415 if main then
416 module:add_identity(category, type_, name)
417 else
418 table.insert(bare_identities, {category=category, type=type_, name=name})
419 end
420 end
421 for extension in query:childtags("x", _DATA_NS) do
422 if main then
423 module:add_extension(extension)
424 else
425 table.insert(bare_extensions, extension)
426 end
427 end
428 end
429
430 function disco_error(event)
431 local stanza = event.stanza
432 if stanza.attr.to ~= module.host then
433 module:log("warn", 'Stanza result has "to" attribute not addressed to current host, id conflict ?')
434 return
435 end
436 module:unhook("iq-result/host/"..stanza.attr.id, disco_result)
437 module:unhook("iq-error/host/"..stanza.attr.id, disco_error)
438 module:log("warn", "Got an error while requesting disco for nesting to "..stanza.attr.from)
439 module:log("warn", "Ignoring disco nesting")
440 end
441
442 function disco_nest(namespace, entity_jid)
443 -- manage discovery nesting (see § 7.2)
444
445 -- first we reset the current values
446 if module.items then
447 module.items['feature'] = nil
448 module.items['identity'] = nil
449 module.items['extension'] = nil
450 bare_features = set.new()
451 bare_identities = {}
452 bare_extensions = {}
453 end
454
455 for _, prefix in ipairs(_PREFIXES) do
456 local node = prefix..namespace
457
458 local iq = st.iq({from=module.host, to=entity_jid, type='get'})
459 :tag('query', {xmlns=_DISCO_NS, node=node})
460
461 local iq_id = iq.attr.id
462
463 module:hook("iq-result/host/"..iq_id, disco_result)
464 module:hook("iq-error/host/"..iq_id, disco_error)
465 module:send(iq)
466 end
467 end
468
469 -- disco to bare jids special case
470
471 module:hook("account-disco-info", function(event)
472 -- this event is called when a disco info request is done on a bare jid
473 -- we get the final reply and filter delegated features/identities/extensions
474 local reply = event.reply;
475 reply.tags[1]:maptags(function(child)
476 if child.name == 'feature' then
477 local feature_ns = child.attr.var
478 for namespace, _ in pairs(ns_delegations) do
479 if string.sub(feature_ns, 1, #namespace) == namespace then
480 module:log("debug", "Removing feature namespace %s which is delegated", feature_ns)
481 return nil
482 end
483 end
484 elseif child.name == 'identity' then
485 for item in disabled_identities:items() do
486 if item.category == child.attr.category
487 and item.type == child.attr.type
488 -- we don't check name, because mod_pep use a name for main disco, but not in account-disco-info hook
489 -- and item.name == child.attr.name
490 then
491 module:log("debug", "Removing (%s/%s%s) identity because of delegation", item.category, item.type, item.name and "/"..item.name or "")
492 return nil
493 end
494 end
495 elseif child.name == 'x' and child.attr.xmlns == _DATA_NS then
496 local form_type = find_form_type(child)
497 if form_type then
498 for namespace, _ in pairs(ns_delegations) do
499 if string.sub(form_type, 1, #namespace) == namespace then
500 module:log("debug", "Removing extension which is delegated: %s", tostring(child))
501 return nil
502 end
503 end
504 end
505
506 end
507 return child
508 end)
509 for feature in bare_features:items() do
510 reply:tag('feature', {var=feature}):up()
511 end
512 for _, item in ipairs(bare_identities) do
513 reply:tag('identity', {category=item.category, type=item.type, name=item.name}):up()
514 end
515 for _, stanza in ipairs(bare_extensions) do
516 reply:add_child(stanza)
517 end
518
519 end, -2^32);