Files
2026-09-06 13:51:11 +00:00

98 lines
4.5 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Probe: XEP-0356 mod_privilege functional test against a Prosody 13.0 stand.
Connects as an external component (XEP-0114) telegram.test.nixg.ru, then sends a
privileged roster IQ-get on behalf of user testuser1@test.nixg.ru.
- mod_privilege working -> server replies type=result with the roster
- not allowed -> server replies a forbidden/not-allowed error stanza
Verified output (2026-08-30, stand /opt/icq/test, container icq-prosody-test):
>>> component connected as telegram.test.nixg.ru
>>> on_iq fired: type=result, id=priv-roster-1
>>> PRIVILEGE OK: mod_privilege ответил result (roster testuser1)
NOTE: a trailing ">>> TIMEOUT" line is COSMETIC — comp.disconnect() races
wait_until('disconnected'); the result line above was already printed.
Expected stand config (full recipe: references/prosody-13-parallel-stand.md):
- external component: module-less `Component "telegram.test.nixg.ru"` + component_secret
(raises the 5347 listener — component_ports was REMOVED in 13.0)
- VirtualHost: modules_enabled = { "privilege", ... } + privileged_entities for the component
- host port 15347 -> container 5347 (component listener)
"""
import asyncio, logging, ssl
import slixmpp
from slixmpp import ComponentXMPP
from slixmpp.xmlstream import ET
from slixmpp.xmlstream.handler import Callback
from slixmpp.xmlstream.matcher import MatchXPath
HOST_PORT = 15347 # host port mapped to container's 5347
COMPONENT_JID = "telegram.test.nixg.ru"
COMPONENT_SECRET = "test-secret-telegram-123"
SERVER_DOMAIN = "test.nixg.ru"
TARGET_USER = "testuser1@test.nixg.ru"
logging.basicConfig(level=logging.WARNING, format='%(levelname)s %(name)s: %(message)s')
# Set to DEBUG to see the raw XML exchange (RECV shows the <privilege> advert and the IQ result):
# logging.getLogger('slixmpp.xmlstream.xmlstream').setLevel(logging.DEBUG)
async def main():
comp = ComponentXMPP(COMPONENT_JID, COMPONENT_SECRET, SERVER_DOMAIN)
comp.ssl_context = ssl.create_default_context()
comp.ssl_context.check_hostname = False
comp.ssl_context.verify_mode = ssl.CERT_NONE
def on_iq(ev):
# The raw Callback on {jabber:component:accept}iq delivers a plain XML STRING,
# NOT a stanza object — parse it before reading attributes/children.
if isinstance(ev, str):
ev = ET.fromstring(ev)
print(f">>> on_iq fired: type={ev.get('type')}, id={ev.get('id')}")
if ev.get('type') in ('result', 'error') and ev.get('id') == 'priv-roster-1':
if ev.get('type') == 'result':
print(f">>> PRIVILEGE OK: mod_privilege ответил result (roster {TARGET_USER})")
for q in ev.findall('{jabber:iq:roster}query'):
for it in q.findall('{jabber:iq:roster}item'):
print(f" - {it.get('jid')} (sub={it.get('subscription')})")
if not ev.findall('{jabber:iq:roster}query/*'):
print(" (roster пуст — у testuser1 нет контактов)")
else:
conds = ev.findall('{urn:ietf:params:xml:ns:xmpp-stanzas}*')
cond = conds[0].tag.split('}')[1] if conds else '?'
print(f">>> PRIVILEGE FAILED: {cond}")
comp.disconnect()
def on_session_start(_ev):
print(f">>> component connected as {COMPONENT_JID}")
iq = comp.Iq()
iq['id'] = 'priv-roster-1'
iq['type'] = 'get'
iq['to'] = TARGET_USER
iq['from'] = COMPONENT_JID
iq.append(ET.Element('{jabber:iq:roster}query'))
comp.send(iq)
comp.add_event_handler('session_start', on_session_start)
comp.add_event_handler('disconnected', lambda ev: print(">>> component disconnected"))
comp.add_event_handler('connection_failed', lambda ev: print(">>> connection_failed", ev))
# slixmpp 1.17 ComponentXMPP registers ONLY handshake + presence_probe handlers —
# incoming IQ stanzas NEVER fire add_event_handler('iq', ...), even though the XML
# arrives (visible in xmlstream DEBUG). Must register a raw Callback instead:
comp.register_handler(
Callback('IQPriv', MatchXPath('{jabber:component:accept}iq'), on_iq))
try:
await asyncio.wait_for(comp.connect('127.0.0.1', HOST_PORT), 15)
await asyncio.wait_for(comp.wait_until('disconnected'), 30)
except asyncio.TimeoutError:
print(">>> TIMEOUT (нет ответа от mod_privilege)")
except Exception as e:
print(f">>> EXC: {type(e).__name__}: {e}")
asyncio.run(main())