Mercurial > prosody-modules
view mod_debug_omemo/mod_debug_omemo.lua @ 5668:ecfd7aece33b
mod_measure_modules: Report module statuses via OpenMetrics
Someone in the chat asked about a health check endpoint, which reminded
me of mod_http_status, which provides access to module statuses with
full details. After that, this idea came about, which seems natural.
As noted in the README, it could be used to monitor that critical
modules are in fact loaded correctly.
As more modules use the status API, the more useful this module and
mod_http_status becomes.
author | Kim Alvefur <zash@zash.se> |
---|---|
date | Fri, 06 Oct 2023 18:34:39 +0200 |
parents | ecfffbbcbf42 |
children |
line wrap: on
line source
local array = require "util.array"; local jid = require "util.jid"; local set = require "util.set"; local st = require "util.stanza"; local url_escape = require "util.http".urlencode; local base_url = "https://"..module.host.."/"; local render_html_template = require"util.interpolation".new("%b{}", st.xml_escape, { urlescape = url_escape; lower = string.lower; classname = function (s) return (s:gsub("%W+", "-")); end; relurl = function (s) if s:match("^%w+://") then return s; end return base_url.."/"..s; end; }); local render_url = require "util.interpolation".new("%b{}", url_escape, { urlescape = url_escape; noscheme = function (url) return (url:gsub("^[^:]+:", "")); end; }); local mod_pep = module:depends("pep"); local mam = module:open_store("archive", "archive"); local function get_user_omemo_info(username) local everything_valid = true; local any_device = false; local omemo_status = {}; local omemo_devices; local pep_service = mod_pep.get_pep_service(username); if pep_service and pep_service.nodes then local ok, _, device_list = pep_service:get_last_item("eu.siacs.conversations.axolotl.devicelist", true); if ok and device_list then device_list = device_list:get_child("list", "eu.siacs.conversations.axolotl"); end if device_list then omemo_devices = {}; for device_entry in device_list:childtags("device") do any_device = true; local device_info = {}; local device_id = tonumber(device_entry.attr.id or ""); if device_id then device_info.id = device_id; local bundle_id = ("eu.siacs.conversations.axolotl.bundles:%d"):format(device_id); local have_bundle, _, bundle = pep_service:get_last_item(bundle_id, true); if have_bundle and bundle and bundle:get_child("bundle", "eu.siacs.conversations.axolotl") then device_info.have_bundle = true; local config_ok, bundle_config = pep_service:get_node_config(bundle_id, true); if config_ok and bundle_config then device_info.bundle_config = bundle_config; if bundle_config.max_items == 1 and bundle_config.access_model == "open" and bundle_config.persist_items == true and bundle_config.publish_model == "publishers" then device_info.valid = true; end end end end if device_info.valid == nil then device_info.valid = false; everything_valid = false; end table.insert(omemo_devices, device_info); end local config_ok, list_config = pep_service:get_node_config("eu.siacs.conversations.axolotl.devicelist", true); if config_ok and list_config then omemo_status.config = list_config; if list_config.max_items == 1 and list_config.access_model == "open" and list_config.persist_items == true and list_config.publish_model == "publishers" then omemo_status.config_valid = true; end end if omemo_status.config_valid == nil then omemo_status.config_valid = false; everything_valid = false; end end end omemo_status.valid = everything_valid and any_device; return { status = omemo_status; devices = omemo_devices; }; end local access_model_text = { open = "Public"; whitelist = "Private"; roster = "Contacts only"; presence = "Contacts only"; }; local function get_message(username, message_id) if mam.get then return mam:get(username, message_id); end -- COMPAT local message; for _, result in mam:find(username, { key = message_id }) do message = result; end return message; end local function render_message(event, path) local username, message_id = path:match("^([^/]+)/(.+)$"); if not username then return 400; end local message = get_message(username, message_id); if not message then return 404; end local user_omemo_status = get_user_omemo_info(username); local user_rids = set.new(array.pluck(user_omemo_status.devices or {}, "id")) / tostring; local message_omemo_header = message:find("{eu.siacs.conversations.axolotl}encrypted/header"); local message_rids = set.new(); local rid_info = {}; if message_omemo_header then for key_el in message_omemo_header:childtags("key") do local rid = key_el.attr.rid; if rid then message_rids:add(rid); local prekey = key_el.attr.prekey; rid_info = { prekey = prekey and (prekey == "1" or prekey:lower() == "true"); }; end end end local rids = user_rids + message_rids; local direction = jid.bare(message.attr.to) == (username.."@"..module.host) and "incoming" or "outgoing"; local is_encrypted = not not message_omemo_header; local sender_id = message_omemo_header and message_omemo_header.attr.sid or nil; local f = module:load_resource("view.tpl.html"); if not f then return 500; end local tpl = f:read("*a"); local data = { user = username, rids = {} }; for rid in rids do data.rids[rid] = { status = message_rids:contains(rid) and "Encrypted" or user_rids:contains(rid) and "Missing" or nil; prekey = rid_info.prekey; }; end data.message = { type = message.attr.type or "normal"; direction = direction; encryption = is_encrypted and "encrypted" or "unencrypted"; has_any_keys = not message_rids:empty(); has_no_keys = message_rids:empty(); }; data.omemo = { sender_id = sender_id; status = user_omemo_status.status.valid and "no known issues" or "problems"; }; data.omemo.devices = {}; if user_omemo_status.devices then for _, device_info in ipairs(user_omemo_status.devices) do data.omemo.devices[("%d"):format(device_info.id)] = { status = device_info.valid and "OK" or "Problem"; bundle = device_info.have_bundle and "Published" or "Missing"; access_model = access_model_text[device_info.bundle_config and device_info.bundle_config.access_model or nil]; }; end else data.omemo.devices[false] = { status = "No devices have published OMEMO keys on this account" }; end event.response.headers.content_type = "text/html; charset=utf-8"; return render_html_template(tpl, data); end local function check_omemo_fallback(event) local message = event.stanza; local message_omemo_header = message:find("{eu.siacs.conversations.axolotl}encrypted/header"); if not message_omemo_header then return; end local to_bare = jid.bare(message.attr.to); local archive_stanza_id; for stanza_id_tag in message:childtags("stanza-id", "urn:xmpp:sid:0") do if stanza_id_tag.attr.by == to_bare then archive_stanza_id = stanza_id_tag.attr.id; end end if not archive_stanza_id then return; end local debug_url = render_url(module:http_url().."/view/{username}/{message_id}", { username = jid.node(to_bare); message_id = archive_stanza_id; }); local body = message:get_child("body"); if not body then body = st.stanza("body") :text("This message is encrypted using OMEMO, but could not be decrypted by your device.\nFor more information see: "..debug_url); message:reset():add_child(body); else body:text("\n\nOMEMO debug information: "..debug_url); end end module:hook("message/bare", check_omemo_fallback, -0.5); module:hook("message/full", check_omemo_fallback, -0.5); module:depends("http") module:provides("http", { route = { ["GET /view/*"] = render_message; }; });