Mercurial > prosody-modules
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); |