Files
xmpp-server-prosody/references/prosody-13-parallel-stand.md
2026-09-06 13:51:11 +00:00

7.5 KiB

Prosody 13.0 parallel test stand (icq-prosody-test) — recipe + state

Goal: validate Prosody 13.0 (custom image gitea.nixg.ru/hermes/icq-prosody:13.0) in parallel with the production 0.11.9 container, WITHOUT touching prod. Checked: authorization (works), mod_privilege (XEP-0356) functional test — PASSED 2026-08-30: external component got privileges and a privileged roster IQ got type=result.

Layout (on bigbox, /opt/icq/test/prosody)

/opt/icq/test/prosody/
  config/prosody.cfg.lua   # test.nixg.ru, ports 15222/15269/15280, component secret
  config/certs/            # self-signed test.nixg.ru.{crt,key}
  data/                    # prosody data (URL-encoded: data/test%2enixg%2eru/accounts/)
  logs/prosody.log|err
  auth_test.py             # slixmpp auth tests (passes)
  privilege_test.py        # external-component mod_privilege probe (PASSES)

Container

docker run -d --name icq-prosody-test --restart unless-stopped -h test.nixg.ru \
  -v /opt/icq/test/prosody/config:/etc/prosody \
  -v /opt/icq/test/prosody/data:/var/lib/prosody \
  -v /opt/icq/test/prosody/logs:/var/log/prosody \
  -p 15222:15222 -p 15269:15269 -p 15280:15280 \
  -p 15347:5347 \
  gitea.nixg.ru/hermes/icq-prosody:13.0

NOTE: on 13.0 the component listener binds 5347 inside the container (component_ports removed — see SKILL.md). Publish as 15347:5347.

Key config: external component (module-less Component) + secrets

component_secrets = {
    ["telegram.test.nixg.ru"] = "test-secret-telegram-123",
}
-- 13.0 requires the explicit external-component block to raise the 5347 listener:
-- Component "telegram.test.nixg.ru"      -- NO module name => external XEP-0114
--     component_secret = "test-secret-telegram-123";

pubsub (13.0): Component "pubsub.test.nixg.ru" "pubsub" — NOT a VirtualHost module.

Auth test (slixmpp — verified WORKING on 13.0)

  • slixmpp must skip cert verification on the self-signed stand:
    self.ssl_context = ssl.create_default_context()
    self.ssl_context.check_hostname = False
    self.ssl_context.verify_mode = ssl.CERT_NONE
    
  • Force non-standard port the RELIABLE way: await x.connect('127.0.0.1', 15222). x.address = ... is IGNORED (DNS lookup of the JID domain wins; it dialed the public IP :5222 → connect errors). custom_address attribute also did not work in slixmpp 1.17.0 — the positional args to connect() are the only thing that worked.
  • Results: testuser1/testuser2/admin AUTH OK; wrong password → failed_auth → "AUTH FAILED".
  • Handler registration differs: ComponentXMPP has NO add_handler/make_iq_get(queryns=...). Use iq = comp.Iq('get', id) + iq.append(ET.Element('{jabber:iq:roster}query')). NOTE: comp.add_event_handler('iq', ...) does NOT fire for incoming IQ on ComponentXMPP — register a raw Callback on {jabber:component:accept}iq instead (see mod_privilege section below).

mod_privilege (XEP-0356) status — PASSED

  • NOT in Prosody core in 13.0 (stock image has zero privilege files). Community module.
  • 13.0 needs the prosody-modules TIP version (promise API) — the old 0.11.9 callback stub is incompatible (:next vs callback). hg.prosody.im/prosody-modules/raw-file/tip/mod_privilege/mod_privilege.lua (~685 lines).
  • The custom image embeds it (prosody-docker/modules/mod_privilege.lua in the hermes/icq repo; workflow builds via Gitea Actions).

Working config (exact, verified)

-- GLOBAL: component_interfaces MUST be global (above VirtualHost/Component), else listener stays 127.0.0.1
component_interfaces = { "0.0.0.0" }

-- EXTERNAL COMPONENT block — module-less => XEP-0114; raises the 5347 listener
Component "telegram.test.nixg.ru"
    component_secret = "test-secret-telegram-123"
    modules_enabled = { "privilege" }   -- so mod_privilege sees component-authenticated

-- VirtualHost: mod_privilege MUST also be in the host's modules_enabled,
-- and privileged_entities lives HERE (host scope, NOT component scope)
VirtualHost "test.nixg.ru"
    modules_enabled = { "privilege", ... }
    privileged_entities = {
        ["telegram.test.nixg.ru"] = {
            roster = "both",           -- perm access=roster type=both
            message = "outgoing",
            iq = { { namespace = "http://jabber.org/protocol/pubsub", type = "both" }, ... }
        }
    }

Trigger: log = { debug = "/var/log/prosody/prosody.log", error = "/var/log/prosody/prosody.err" } (or info = ...) — then the debug lines prove it end-to-end:

test.nixg.ru:privilege   debug   Entity is privileged
test.nixg.ru:privilege   debug   Roster get from allowed privileged entity received

Gotchas found on 13.0 (IMPORTANT)

  1. component_ports removed entirely in 13.0 (not a single grep hit in /usr/lib/prosody). Component listener = hardcoded 5347 (default_port = 5347 in mod_component.lua). Publish as e.g. 15347:5347.
  2. component_secrets alone does NOT activate the listener. You MUST have the module-less Component "jid" block (mod_component loads only for an actual external-component host).
  3. component_interfaces is global-scope only. Put it at the top of the config, NOT inside a VirtualHost/Component block, or it is silently ignored (check config complains "Problems found").
  4. log block: only ONE log = {...} allowed. A second log= later in the file OVERWRITES the first (Lua). If first had info = file and second debug = console, info-file logging silently dies. Keep one log block in the global section, info = file, error = file.
  5. Component session has NO full_jid (mod_component sets only session.host). The tip mod_privilege logs Entity nil try to get roster without permission when privileges are missing — but with the config above privileges ARE granted; "Entity is privileged" debug appears. The "Entity nil" line is a log cosmetic, not a privileges failure.
  6. mod_privilege must be in modules_enabled BOTH on the Component block AND on the VirtualHost. Loaded only globally (outside any host) it won't resolve privileged_entities from the VirtualHost.

slixmpp 1.17 ComponentXMPP — in-band IQ gotcha

ComponentXMPP registers ONLY handshake + presence_probe handlers — incoming IQ stanzas are NOT processed (add_event_handler('iq', ...) never fires). The XML arrives fine (visible in xmlstream DEBUG), but no callback runs. Fix: register your own Callback on the raw IQ path:

from slixmpp.xmlstream.handler import Callback
from slixmpp.xmlstream.matcher import MatchXPath
comp.register_handler(Callback('IQPriv', MatchXPath('{jabber:component:accept}iq'), on_iq))

...and note the callback receives the IQ as a RAW XML STRING, not a stanza object — parse with ET.fromstring(...) then elem.get('type') / elem.findall('{jabber:iq:roster}item').

Verified test output (2026-08-30)

>>> component connected as telegram.test.nixg.ru
>>> on_iq fired: type=result, id=priv-roster-1
>>> PRIVILEGE OK: mod_privilege ответил result (roster testuser1)

(The trailing "TIMEOUT" in the probe is cosmetic — disconnect() races wait_until('disconnected'); the result was already received.)

Version sanity checks (recap)

  • prod = prosody/prosody:latest = 0.11.9 (the buggy mod_privilege era)
  • image = hermes/icq-prosody:13.0 (Debian trixie-slim base, apt prosody 13.0, 5 custom modules)
  • runner = gitea/runner:3.3.1; job image docker27-bash (docker:27 + bash) — runner shells run steps via bash; Alpine docker:27 has no bash → exit 127. Use GITHUB_TOKEN for clone, PKG_TOKEN (write:package) only for registry login.