Initial commit: Hermes skill xmpp-server-prosody

This commit is contained in:
estorozhenko
2026-09-06 13:51:11 +00:00
commit 3da6c8709b
28 changed files with 2016 additions and 0 deletions
+155
View File
@@ -0,0 +1,155 @@
# 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.