# 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 ```bash 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 ```lua 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: ```python 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) ```lua -- 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: ```python 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.