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); |
