mirror of
https://gitverse.ru/kpa39l/xmpp-server-prosody.git
synced 2026-09-29 09:45:02 +00:00
98 lines
4.5 KiB
Python
98 lines
4.5 KiB
Python
#!/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()) |