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 --- plugins/mod_external_services.lua | 205 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 205 insertions(+) create mode 100644 plugins/mod_external_services.lua (limited to 'plugins/mod_external_services.lua') 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); -- cgit v1.2.3 From 6dfae9bbfa4b60374a52482bdf60355acafd5a6a Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Sat, 25 Jul 2020 10:22:37 +0200 Subject: mod_external_services: Support adding services via items API --- plugins/mod_external_services.lua | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'plugins/mod_external_services.lua') diff --git a/plugins/mod_external_services.lua b/plugins/mod_external_services.lua index 51d2d313..c4762e65 100644 --- a/plugins/mod_external_services.lua +++ b/plugins/mod_external_services.lua @@ -105,7 +105,8 @@ local function handle_services(event) end local reply = st.reply(stanza):tag("services", { xmlns = action.attr.xmlns }); - local services = configured_services / prepare; + local extras = module:get_host_items("external_service"); + local services = ( configured_services + extras ) / prepare; local requested_type = action.attr.type; if requested_type then @@ -149,7 +150,8 @@ local function handle_credentials(event) end local reply = st.reply(stanza):tag("credentials", { xmlns = action.attr.xmlns }); - local services = configured_services / prepare; + local extras = module:get_host_items("external_service"); + local services = ( configured_services + extras ) / prepare; services:filter(function (item) return item.restricted; end) -- cgit v1.2.3 From 0b65dea7c01b57dc036de2c62de3dd8e55c3f293 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Sat, 25 Jul 2020 12:09:19 +0200 Subject: mod_external_services: Prepare to allow more credential algorithms Not sure what algorithms might fit here. Separation makes some sense. This is also a preparation for having a callback. (See next commit) --- plugins/mod_external_services.lua | 38 +++++++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 13 deletions(-) (limited to 'plugins/mod_external_services.lua') diff --git a/plugins/mod_external_services.lua b/plugins/mod_external_services.lua index c4762e65..e1c72387 100644 --- a/plugins/mod_external_services.lua +++ b/plugins/mod_external_services.lua @@ -14,6 +14,27 @@ local configured_services = module:get_option_array("external_services", {}); local access = module:get_option_set("external_service_access", {}); +-- https://tools.ietf.org/html/draft-uberti-behave-turn-rest-00 +local function behave_turn_rest_credentials(srv, item, secret) + local ttl = default_ttl; + if type(item.ttl) == "number" then + ttl = item.ttl; + end + local expires = srv.expires or os.time() + ttl; + 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)); +end + +local algorithms = { + turn = behave_turn_rest_credentials; +} + -- filter config into well-defined service records local function prepare(item) if type(item) ~= "table" then @@ -63,24 +84,15 @@ local function prepare(item) 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_cb = algorithms[item.algorithm] or algorithms[srv.type]; 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); + if secret_cb then + secret_cb(srv, item, secret); + srv.restricted = true; end - srv.username = username; - srv.password = base64.encode(hashes.hmac_sha1(secret, srv.username)); - srv.restricted = true; end return srv; end -- cgit v1.2.3 From 5bc6130e57fe2af3108ec538b83768100bdc177c Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Sat, 25 Jul 2020 12:22:03 +0200 Subject: mod_external_services: Allow specifying a credential generation callback This is especially targeted at services added via the items API. More involved credential generation should use the event hook. --- plugins/mod_external_services.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'plugins/mod_external_services.lua') diff --git a/plugins/mod_external_services.lua b/plugins/mod_external_services.lua index e1c72387..7c18e326 100644 --- a/plugins/mod_external_services.lua +++ b/plugins/mod_external_services.lua @@ -84,7 +84,7 @@ local function prepare(item) srv.expires = os.time() + item.ttl; end if (item.secret == true and default_secret) or type(item.secret) == "string" then - local secret_cb = algorithms[item.algorithm] or algorithms[srv.type]; + local secret_cb = item.credentials_cb or algorithms[item.algorithm] or algorithms[srv.type]; local secret = item.secret; if secret == true then secret = default_secret; -- cgit v1.2.3 From fb6c098ed661d4bd14f1a0515e4ae6b110c76b19 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Mon, 17 Aug 2020 00:24:11 +0200 Subject: mod_external_services: Validate services added via events While writing developer documentation it became obvious that i was silly to have one item format for config and items API, and another format for the event API. Then there's the stanza format, but that's a common pattern. This change reduces the possible input formats to two and allows other modules the benefit of the processing and validation performed on items from the config. --- plugins/mod_external_services.lua | 14 ++++++++++++++ 1 file changed, 14 insertions(+) (limited to 'plugins/mod_external_services.lua') diff --git a/plugins/mod_external_services.lua b/plugins/mod_external_services.lua index 7c18e326..e18e7c3e 100644 --- a/plugins/mod_external_services.lua +++ b/plugins/mod_external_services.lua @@ -4,6 +4,7 @@ local base64 = require "util.encodings".base64; local hashes = require "util.hashes"; local st = require "util.stanza"; local jid = require "util.jid"; +local array = require "util.array"; local default_host = module:get_option_string("external_service_host", module.host); local default_port = module:get_option_number("external_service_port"); @@ -105,6 +106,14 @@ function module.load() end end +-- Ensure only valid items are added in events +local services_mt = { + __index = getmetatable(array()).__index; + __newindex = function (self, i, v) + rawset(self, i, assert(prepare(v), "Invalid service entry added")); + end; +} + local function handle_services(event) local origin, stanza = event.origin, event.stanza; local action = stanza.tags[1]; @@ -127,6 +136,8 @@ local function handle_services(event) end); end + setmetatable(services, services_mt); + module:fire_event("external_service/services", { origin = origin; stanza = stanza; @@ -177,6 +188,9 @@ local function handle_credentials(event) }); end + setmetatable(services, services_mt); + setmetatable(requested_credentials, services_mt); + module:fire_event("external_service/credentials", { origin = origin; stanza = stanza; -- cgit v1.2.3