From b289d05cfbde014799fcf66fec36895bee5be071 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Sat, 18 Jul 2020 15:36:25 +0200 Subject: mod_external_services: XEP-0215: External Service Discovery --- CHANGES | 1 + doc/doap.xml | 9 ++ plugins/mod_external_services.lua | 205 ++++++++++++++++++++++++++++++++++++++ spec/scansion/extdisco.scs | 57 +++++++++++ spec/scansion/prosody.cfg.lua | 12 +++ 5 files changed, 284 insertions(+) create mode 100644 plugins/mod_external_services.lua create mode 100644 spec/scansion/extdisco.scs diff --git a/CHANGES b/CHANGES index d1e50abb..080ee3d7 100644 --- a/CHANGES +++ b/CHANGES @@ -17,6 +17,7 @@ TRUNK - `daemonize` option deprecated - SASL DIGEST-MD5 removed - Switch to libunbound for DNS queries +- mod_external_services (XEP-0215) 0.11.0 ====== diff --git a/doc/doap.xml b/doc/doap.xml index ef1437e7..6024903f 100644 --- a/doc/doap.xml +++ b/doc/doap.xml @@ -414,6 +414,15 @@ required level + + + + 0.7 + complete + 0.12 + mod_external_services + + diff --git a/plugins/mod_external_services.lua b/plugins/mod_external_services.lua new file mode 100644 index 00000000..51d2d313 --- /dev/null +++ b/plugins/mod_external_services.lua @@ -0,0 +1,205 @@ + +local dt = require "util.datetime"; +local base64 = require "util.encodings".base64; +local hashes = require "util.hashes"; +local st = require "util.stanza"; +local jid = require "util.jid"; + +local default_host = module:get_option_string("external_service_host", module.host); +local default_port = module:get_option_number("external_service_port"); +local default_secret = module:get_option_string("external_service_secret"); +local default_ttl = module:get_option_number("external_service_ttl", 86400); + +local configured_services = module:get_option_array("external_services", {}); + +local access = module:get_option_set("external_service_access", {}); + +-- filter config into well-defined service records +local function prepare(item) + if type(item) ~= "table" then + module:log("error", "Service definition is not a table: %q", item); + return nil; + end + + local srv = { + type = nil; + transport = nil; + host = default_host; + port = default_port; + username = nil; + password = nil; + restricted = nil; + expires = nil; + }; + + if type(item.type) == "string" then + srv.type = item.type; + else + module:log("error", "Service missing mandatory 'type' field: %q", item); + return nil; + end + if type(item.transport) == "string" then + srv.transport = item.transport; + end + if type(item.host) == "string" then + srv.host = item.host; + end + if type(item.port) == "number" then + srv.port = item.port; + end + if type(item.username) == "string" then + srv.username = item.username; + end + if type(item.password) == "string" then + srv.password = item.password; + srv.restricted = true; + end + if item.restricted == true then + srv.restricted = true; + end + if type(item.expires) == "number" then + srv.expires = item.expires; + elseif type(item.ttl) == "number" then + srv.expires = os.time() + item.ttl; + end + if (item.secret == true and default_secret) or type(item.secret) == "string" then + local ttl = default_ttl; + if type(item.ttl) == "number" then + ttl = item.ttl; + end + local expires = os.time() + ttl; + local secret = item.secret; + if secret == true then + secret = default_secret; + end + local username; + if type(item.username) == "string" then + username = string.format("%d:%s", expires, item.username); + else + username = string.format("%d", expires); + end + srv.username = username; + srv.password = base64.encode(hashes.hmac_sha1(secret, srv.username)); + srv.restricted = true; + end + return srv; +end + +function module.load() + -- Trigger errors on startup + local services = configured_services / prepare; + if #services == 0 then + module:log("warn", "No services configured or all had errors"); + end +end + +local function handle_services(event) + local origin, stanza = event.origin, event.stanza; + local action = stanza.tags[1]; + + local user_bare = jid.bare(stanza.attr.from); + local user_host = jid.host(user_bare); + if not ((access:empty() and origin.type == "c2s") or access:contains(user_bare) or access:contains(user_host)) then + origin.send(st.error_reply(stanza, "auth", "forbidden")); + return true; + end + + local reply = st.reply(stanza):tag("services", { xmlns = action.attr.xmlns }); + local services = configured_services / prepare; + + local requested_type = action.attr.type; + if requested_type then + services:filter(function(item) + return item.type == requested_type; + end); + end + + module:fire_event("external_service/services", { + origin = origin; + stanza = stanza; + reply = reply; + requested_type = requested_type; + services = services; + }); + + for _, srv in ipairs(services) do + reply:tag("service", { + type = srv.type; + transport = srv.transport; + host = srv.host; + port = srv.port and string.format("%d", srv.port) or nil; + username = srv.username; + password = srv.password; + expires = srv.expires and dt.datetime(srv.expires) or nil; + restricted = srv.restricted and "1" or nil; + }):up(); + end + + origin.send(reply); + return true; +end + +local function handle_credentials(event) + local origin, stanza = event.origin, event.stanza; + local action = stanza.tags[1]; + + if origin.type ~= "c2s" then + origin.send(st.error_reply(stanza, "auth", "forbidden")); + return true; + end + + local reply = st.reply(stanza):tag("credentials", { xmlns = action.attr.xmlns }); + local services = configured_services / prepare; + services:filter(function (item) + return item.restricted; + end) + + local requested_credentials = {}; + for service in action:childtags("service") do + table.insert(requested_credentials, { + type = service.attr.type; + host = service.attr.host; + port = tonumber(service.attr.port); + }); + end + + module:fire_event("external_service/credentials", { + origin = origin; + stanza = stanza; + reply = reply; + requested_credentials = requested_credentials; + services = services; + }); + + for req_srv in action:childtags("service") do + for _, srv in ipairs(services) do + if srv.type == req_srv.attr.type and srv.host == req_srv.attr.host + and not req_srv.attr.port or srv.port == tonumber(req_srv.attr.port) then + reply:tag("service", { + type = srv.type; + transport = srv.transport; + host = srv.host; + port = srv.port and string.format("%d", srv.port) or nil; + username = srv.username; + password = srv.password; + expires = srv.expires and dt.datetime(srv.expires) or nil; + restricted = srv.restricted and "1" or nil; + }):up(); + end + end + end + + origin.send(reply); + return true; +end + +-- XEP-0215 v0.7 +module:add_feature("urn:xmpp:extdisco:2"); +module:hook("iq-get/host/urn:xmpp:extdisco:2:services", handle_services); +module:hook("iq-get/host/urn:xmpp:extdisco:2:credentials", handle_credentials); + +-- COMPAT XEP-0215 v0.6 +-- Those still on the old version gets to deal with undefined attributes until they upgrade. +module:add_feature("urn:xmpp:extdisco:1"); +module:hook("iq-get/host/urn:xmpp:extdisco:1:services", handle_services); +module:hook("iq-get/host/urn:xmpp:extdisco:1:credentials", handle_credentials); diff --git a/spec/scansion/extdisco.scs b/spec/scansion/extdisco.scs new file mode 100644 index 00000000..fd73c9da --- /dev/null +++ b/spec/scansion/extdisco.scs @@ -0,0 +1,57 @@ +# XEP-0215: External Service Discovery + +[Client] Romeo + password: password + jid: user@localhost/mFquWxSr + +----- + +Romeo connects + +Romeo sends: + + + + +Romeo receives: + + + + + + + + + + +Romeo sends: + + + + +Romeo receives: + + + + + + + +Romeo sends: + + + + + + +Romeo receives: + + + + + + + +Romeo disconnects + +# recording ended on 2020-07-18T16:47:57Z diff --git a/spec/scansion/prosody.cfg.lua b/spec/scansion/prosody.cfg.lua index 0bf68ddb..4e43d312 100644 --- a/spec/scansion/prosody.cfg.lua +++ b/spec/scansion/prosody.cfg.lua @@ -62,6 +62,7 @@ modules_enabled = { --"legacyauth"; -- Legacy authentication. Only used by some old clients and bots. --"proxy65"; -- Enables a file transfer proxy service which clients behind NAT can use "lastactivity"; + "external_services"; -- Useful for testing --"scansion_record"; -- Records things that happen in scansion test case format @@ -77,6 +78,17 @@ contact_info = { support = { "https://localhost/support.html", "xmpp:support@localhost" }; } +external_service_host = "default.example" +external_service_port = 9876 +external_service_secret = "" +external_services = { + {type = "stun"; transport = "udp"}; + {type = "turn"; transport = "udp"; secret = true}; + {type = "turn"; transport = "udp"; secret = "foo"}; + {type = "ftp"; transport = "tcp"; port = 2121; username = "john"; password = "password"}; + {type = "ftp"; transport = "tcp"; host = "ftp.example.com"; port = 21; username = "john"; password = "password"}; +} + modules_disabled = { "s2s"; } -- cgit v1.2.3