Mercurial > prosody-hg
view plugins/mod_dialback.lua @ 11523:5f15ab7c6ae5
Statistics: Rewrite statistics backends to use OpenMetrics
The metric subsystem of Prosody has had some shortcomings from
the perspective of the current state-of-the-art in metric
observability.
The OpenMetrics standard [0] is a formalization of the data
model (and serialization format) of the well-known and
widely-used Prometheus [1] software stack.
The previous stats subsystem of Prosody did not map well to that
format (see e.g. [2] and [3]); the key reason is that it was
trying to do too much math on its own ([2]) while lacking
first-class support for "families" of metrics ([3]) and
structured metric metadata (despite the `extra` argument to
metrics, there was no standard way of representing common things
like "tags" or "labels").
Even though OpenMetrics has grown from the Prometheus world of
monitoring, it maps well to other popular monitoring stacks
such as:
- InfluxDB (labels can be mapped to tags and fields as necessary)
- Carbon/Graphite (labels can be attached to the metric name with
dot-separation)
- StatsD (see graphite when assuming that graphite is used as
backend, which is the default)
The util.statsd module has been ported to use the OpenMetrics
model as a proof of concept. An implementation which exposes
the util.statistics backend data as Prometheus metrics is
ready for publishing in prosody-modules (most likely as
mod_openmetrics_prometheus to avoid breaking existing 0.11
deployments).
At the same time, the previous measure()-based API had one major
advantage: It is really simple and easy to use without requiring
lots of knowledge about OpenMetrics or similar concepts. For that
reason as well as compatibility with existing code, it is preserved
and may even be extended in the future.
However, code relying on the `stats-updated` event as well as
`get_stats` from `statsmanager` will break because the data
model has changed completely; in case of `stats-updated`, the
code will simply not run (as the event was renamed in order
to avoid conflicts); the `get_stats` function has been removed
completely (so it will cause a traceback when it is attempted
to be used).
Note that the measure_*_event methods have been removed from
the module API. I was unable to find any uses or documentation
and thus deemed they should not be ported. Re-implementation is
possible when necessary.
[0]: https://openmetrics.io/
[1]: https://prometheus.io/
[2]: #959
[3]: #960
| author | Jonas Schäfer <jonas@wielicki.name> |
|---|---|
| date | Sun, 18 Apr 2021 11:47:41 +0200 |
| parents | 18685a5e362e |
| children | 15a3db955ad3 |
line wrap: on
line source
-- Prosody IM -- Copyright (C) 2008-2010 Matthew Wild -- Copyright (C) 2008-2010 Waqas Hussain -- -- This project is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. -- local hosts = _G.hosts; local log = module._log; local st = require "util.stanza"; local sha256_hash = require "util.hashes".sha256; local sha256_hmac = require "util.hashes".hmac_sha256; local nameprep = require "util.encodings".stringprep.nameprep; local uuid_gen = require"util.uuid".generate; local xmlns_stream = "http://etherx.jabber.org/streams"; local dialback_requests = setmetatable({}, { __mode = 'v' }); local dialback_secret = sha256_hash(module:get_option_string("dialback_secret", uuid_gen()), true); local dwd = module:get_option_boolean("dialback_without_dialback", false); --- Helper to check that a session peer's certificate is valid function check_cert_status(session) local host = session.direction == "outgoing" and session.to_host or session.from_host local conn = session.conn:socket() local cert if conn.getpeercertificate then cert = conn:getpeercertificate() end return module:fire_event("s2s-check-certificate", { host = host, session = session, cert = cert }); end function module.save() return { dialback_secret = dialback_secret }; end function module.restore(state) dialback_secret = state.dialback_secret; end function generate_dialback(id, to, from) return sha256_hmac(dialback_secret, to .. ' ' .. from .. ' ' .. id, true); end function initiate_dialback(session) -- generate dialback key session.dialback_key = generate_dialback(session.streamid, session.to_host, session.from_host); session.sends2s(st.stanza("db:result", { from = session.from_host, to = session.to_host }):text(session.dialback_key)); session.log("debug", "sent dialback key on outgoing s2s stream"); end function verify_dialback(id, to, from, key) return key == generate_dialback(id, to, from); end module:hook("stanza/jabber:server:dialback:verify", function(event) local origin, stanza = event.origin, event.stanza; if origin.type == "s2sin_unauthed" or origin.type == "s2sin" then -- We are being asked to verify the key, to ensure it was generated by us origin.log("debug", "verifying that dialback key is ours..."); local attr = stanza.attr; if attr.type then module:log("warn", "Ignoring incoming session from %s claiming a dialback key for %s is %s", origin.from_host or "(unknown)", attr.from or "(unknown)", attr.type); return true; end -- COMPAT: Grr, ejabberd breaks this one too?? it is black and white in XEP-220 example 34 --if attr.from ~= origin.to_host then error("invalid-from"); end local type; if verify_dialback(attr.id, attr.from, attr.to, stanza[1]) then type = "valid" else type = "invalid" origin.log("warn", "Asked to verify a dialback key that was incorrect. An imposter is claiming to be %s?", attr.to); end origin.log("debug", "verified dialback key... it is %s", type); origin.sends2s(st.stanza("db:verify", { from = attr.to, to = attr.from, id = attr.id, type = type }):text(stanza[1])); return true; end end); module:hook("stanza/jabber:server:dialback:result", function(event) local origin, stanza = event.origin, event.stanza; if origin.type == "s2sin_unauthed" or origin.type == "s2sin" then -- he wants to be identified through dialback -- We need to check the key with the Authoritative server local attr = stanza.attr; if not attr.to or not attr.from then origin.log("debug", "Missing Dialback addressing (from=%q, to=%q)", attr.from, attr.to); origin:close("improper-addressing"); return true; end local to, from = nameprep(attr.to), nameprep(attr.from); if not hosts[to] then -- Not a host that we serve origin.log("warn", "%s tried to connect to %s, which we don't serve", from, to); origin:close("host-unknown"); return true; elseif not from then origin:close("improper-addressing"); return true; end if dwd and origin.secure then if check_cert_status(origin, from) == false then return elseif origin.cert_chain_status == "valid" and origin.cert_identity_status == "valid" then origin.sends2s(st.stanza("db:result", { to = from, from = to, id = attr.id, type = "valid" })); module:fire_event("s2s-authenticated", { session = origin, host = from }); return true; end end origin.hosts[from] = { dialback_key = stanza[1] }; dialback_requests[from.."/"..origin.streamid] = origin; -- COMPAT: ejabberd, gmail and perhaps others do not always set 'to' and 'from' -- on streams. We fill in the session's to/from here instead. if not origin.from_host then origin.from_host = from; end if not origin.to_host then origin.to_host = to; end origin.log("debug", "asking %s if key %s belongs to them", from, stanza[1]); module:fire_event("route/remote", { from_host = to, to_host = from; stanza = st.stanza("db:verify", { from = to, to = from, id = origin.streamid }):text(stanza[1]); }); return true; end end); module:hook("stanza/jabber:server:dialback:verify", function(event) local origin, stanza = event.origin, event.stanza; if origin.type == "s2sout_unauthed" or origin.type == "s2sout" then local attr = stanza.attr; local dialback_verifying = dialback_requests[attr.from.."/"..(attr.id or "")]; if dialback_verifying and attr.from == origin.to_host then local valid; if attr.type == "valid" then module:fire_event("s2s-authenticated", { session = dialback_verifying, host = attr.from }); valid = "valid"; else -- Warn the original connection that is was not verified successfully log("warn", "authoritative server for %s denied the key", attr.from or "(unknown)"); valid = "invalid"; end if dialback_verifying.destroyed then log("warn", "Incoming s2s session %s was closed in the meantime, so we can't notify it of the dialback result", tostring(dialback_verifying):match("%w+$")); else dialback_verifying.sends2s( st.stanza("db:result", { from = attr.to, to = attr.from, id = attr.id, type = valid }) :text(dialback_verifying.hosts[attr.from].dialback_key)); end dialback_requests[attr.from.."/"..(attr.id or "")] = nil; end return true; end end); module:hook("stanza/jabber:server:dialback:result", function(event) local origin, stanza = event.origin, event.stanza; if origin.type == "s2sout_unauthed" or origin.type == "s2sout" then -- Remote server is telling us whether we passed dialback local attr = stanza.attr; if not hosts[attr.to] then origin:close("host-unknown"); return true; elseif hosts[attr.to].s2sout[attr.from] ~= origin then -- This isn't right origin:close("invalid-id"); return true; end if stanza.attr.type == "valid" then module:fire_event("s2s-authenticated", { session = origin, host = attr.from }); else origin:close("not-authorized", "dialback authentication failed"); end return true; end end); module:hook_tag("urn:ietf:params:xml:ns:xmpp-sasl", "failure", function (origin, stanza) -- luacheck: ignore 212/stanza if origin.external_auth == "failed" then module:log("debug", "SASL EXTERNAL failed, falling back to dialback"); initiate_dialback(origin); return true; end end, 100); module:hook_tag(xmlns_stream, "features", function (origin, stanza) -- luacheck: ignore 212/stanza if not origin.external_auth or origin.external_auth == "failed" then module:log("debug", "Initiating dialback..."); initiate_dialback(origin); return true; end end, 100); module:hook("s2sout-authenticate-legacy", function (event) module:log("debug", "Initiating dialback..."); initiate_dialback(event.origin); return true; end, 100); -- Offer dialback to incoming hosts module:hook("s2s-stream-features", function (data) data.features:tag("dialback", { xmlns='urn:xmpp:features:dialback' }):up(); end);
