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_addressattribute 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=...). Useiq = 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}iqinstead (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 (
:nextvs 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)
component_portsremoved entirely in 13.0 (not a single grep hit in /usr/lib/prosody). Component listener = hardcoded 5347 (default_port = 5347in mod_component.lua). Publish as e.g. 15347:5347.component_secretsalone does NOT activate the listener. You MUST have the module-lessComponent "jid"block (mod_component loads only for an actual external-component host).component_interfacesis 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").logblock: only ONElog = {...}allowed. A second log= later in the file OVERWRITES the first (Lua). If first hadinfo = fileand seconddebug = console, info-file logging silently dies. Keep one log block in the global section,info = file, error = file.- Component session has NO
full_jid(mod_component sets onlysession.host). The tip mod_privilege logsEntity nil try to get roster without permissionwhen 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. - 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_entitiesfrom 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.