OpenSpec: разнести openspec по проектам

This commit is contained in:
2026-09-11 17:33:02 +00:00
parent 6a4ccd3e6d
commit e17dd7f901
18 changed files with 1741 additions and 0 deletions
+57
View File
@@ -0,0 +1,57 @@
#!/usr/bin/env python3
"""Тест авторизации на Prosody 13.0 (test.nixg.ru:15222)."""
import asyncio, sys, logging, ssl
import slixmpp
logging.basicConfig(level=logging.WARNING, format='%(levelname)s %(name)s: %(message)s')
async def try_login(jid, password, expect_ok=True):
class T(slixmpp.ClientXMPP):
def __init__(self):
super().__init__(jid, password)
self.ok = None
# Отключаем проверку сертификата (тестовый стенд, самоподписанный)
self.ssl_context = ssl.create_default_context()
self.ssl_context.check_hostname = False
self.ssl_context.verify_mode = ssl.CERT_NONE
self.add_event_handler('session_start', self.on_start)
self.add_event_handler('failed_auth', self.on_fail)
self.add_event_handler('disconnected', self.on_disc)
self.add_event_handler('connected', self.on_conn)
def on_conn(self, ev): print(f" [connected] {jid}")
def on_start(self, ev):
self.ok = True
print(f" [AUTH OK] {jid} — session_start, full JID={self.boundjid}")
self.disconnect()
def on_fail(self, ev):
self.ok = False
print(f" [AUTH FAILED] {jid} — incorrect credentials rejected correctly")
self.disconnect()
def on_disc(self, ev):
if self.ok is None:
print(f" [DISCONNECTED before auth] {jid} reason={ev}")
x = T()
# Явно указываем хост/порт тестового сервера (15222 = нестандартный c2s)
try:
await x.connect('127.0.0.1', 15222)
await asyncio.wait_for(x.wait_until('session_start' if expect_ok else 'failed_auth'), 15)
except asyncio.TimeoutError:
print(f" [TIMEOUT] {jid} — no auth result in 15s")
x.disconnect()
except Exception as e:
print(f" [ERROR] {jid}: {type(e).__name__}: {e}")
finally:
try: x.disconnect()
except: pass
async def main():
print("=== 1. Верный пароль: testuser1@test.nixg.ru ===")
await try_login("testuser1@test.nixg.ru", "pass12345", True)
print("=== 2. Верный пароль: testuser2@test.nixg.ru ===")
await try_login("testuser2@test.nixg.ru", "pass12345", True)
print("=== 3. Админ ===")
await try_login("admin@test.nixg.ru", "adminpass123", True)
print("=== 4. НЕВЕРНЫЙ пароль (должен отказать) ===")
await try_login("testuser1@test.nixg.ru", "WRONG", False)
asyncio.run(main())
+38
View File
@@ -0,0 +1,38 @@
#!/usr/bin/env python3
"""Тест авторизации с подробным логом — одна попытка."""
import asyncio, logging, ssl
import slixmpp
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s %(levelname)s %(name)s: %(message)s', datefmt='%H:%M:%S')
class T(slixmpp.ClientXMPP):
def __init__(self):
super().__init__("testuser1@test.nixg.ru", "pass12345")
self.ssl_context = ssl.create_default_context()
self.ssl_context.check_hostname = False
self.ssl_context.verify_mode = ssl.CERT_NONE
self.add_event_handler('session_start', self.on_start)
self.add_event_handler('failed_auth', self.on_fail)
self.add_event_handler('disconnected', self.on_disc)
self.add_event_handler('connected', self.on_conn)
self.add_event_handler('ssl_invalid_cert', self.on_ssl)
def on_conn(self, ev): print(">>> connected")
def on_ssl(self, ev): print(">>> ssl_invalid_cert:", ev)
def on_start(self, ev): print(">>> AUTH OK, boundjid=", self.boundjid); self.disconnect()
def on_fail(self, ev): print(">>> AUTH FAILED"); self.disconnect()
def on_disc(self, ev): print(">>> disconnected:", ev)
async def main():
x = T()
try:
await asyncio.wait_for(x.connect('127.0.0.1', 15222), 20)
await asyncio.wait_for(x.wait_until('session_start'), 20)
except asyncio.TimeoutError:
print(">>> TIMEOUT waiting auth")
except Exception as e:
print(">>> EXC:", type(e).__name__, e)
finally:
try: x.disconnect()
except: pass
asyncio.run(main())
+83
View File
@@ -0,0 +1,83 @@
#!/usr/bin/env python3
"""Функциональный тест mod_privilege (XEP-0356) на Prosody 13.0.
Подключается как внешний компонент telegram.test.nixg.ru (XEP-0114),
затем от имени компонента делает привилегированный запрос roster
пользователя testuser1@test.nixg.ru.
Если mod_privilege работает — компонент получит roster.
Если нет — сервер ответит forbidden/not-allowed.
"""
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
logging.basicConfig(level=logging.WARNING, format='%(levelname)s %(name)s: %(message)s')
logging.getLogger('slixmpp.xmlstream.xmlstream').setLevel(logging.DEBUG)
async def main():
comp = ComponentXMPP("telegram.test.nixg.ru", "test-secret-telegram-123",
"test.nixg.ru")
comp.ssl_context = ssl.create_default_context()
comp.ssl_context.check_hostname = False
comp.ssl_context.verify_mode = ssl.CERT_NONE
result = {"done": False}
def on_iq(ev):
# Callback на {jabber:component:accept}iq присылает строку XML, парсим
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':
result['done'] = True
if ev.get('type') == 'result':
print(">>> PRIVILEGE OK: mod_privilege ответил result (roster testuser1)")
# достаём все <item> из <query>
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()
comp.add_event_handler('iq', on_iq)
def on_session_start(ev):
print(">>> component connected as telegram.test.nixg.ru")
# Привилегированный запрос: получить roster testuser1
iq = comp.Iq()
iq['id'] = 'priv-roster-1'
iq['type'] = 'get'
iq['to'] = 'testuser1@test.nixg.ru'
iq['from'] = 'telegram.test.nixg.ru'
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 НЕ обрабатывает входящие IQ (только handshake + presence_probe) —
# регистрируем свой Callback на IQ, чтобы поймать ответ mod_privilege
def _on_iq_cb(iq):
on_iq(iq)
comp.register_handler(
Callback('IQPriv', MatchXPath('{jabber:component:accept}iq'), _on_iq_cb))
try:
await asyncio.wait_for(comp.connect('127.0.0.1', 15347), 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())
+85
View File
@@ -0,0 +1,85 @@
-- Тестовый конфиг Prosody 13.0 (параллельный стенд для проверки mod_privilege)
-- Домен: test.nixg.ru · Порты: 15222 (c2s) / 15269 (s2s) / 15280 (http)
-- Запущен отдельно от прода (icq-prosody на 5222/5269/5280)
data_path = "/var/lib/prosody"
plugin_paths = { "/etc/prosody/modules" }
log = {
debug = "/var/log/prosody/prosody.log";
error = "/var/log/prosody/prosody.err";
}
admins = { "admin@test.nixg.ru" }
modules_enabled = {
"roster"; -- Список контактов
"saslauth"; -- Аутентификация
"tls"; -- TLS
"dialback"; -- s2s dialback
"disco"; -- Service discovery
"carbons"; -- Копии сообщений на все устройства
"pep"; -- Personal eventing (OMEMO, avatars)
"admin_shell"; -- временно: prosodyctl shell для диагностики
"private"; -- Приватные хранилища XML
"blocklist"; -- Чёрный список
"vcard4"; -- vCard 4
"vcard_legacy"; -- vCard 2
"version"; -- Версия сервера
"uptime"; -- Время работы
"time"; -- Время
"ping"; -- XMPP Ping
"register"; -- In-band registration (для теста можно включить)
"adhoc"; -- ad-hoc команды
"admin_adhoc"; -- админ-команды
"bosh"; -- BOSH
"csi"; -- Client State Indication
"offline"; -- Хранение оффлайн-сообщений
"presence"; -- Присутствие
"mam"; -- Message Archive Management
"websocket"; -- WebSocket
"server_contact_info";
}
-- Порты (локальные, чтобы не конфликтовать с продом icq-prosody)
c2s_ports = { 15222 }
s2s_ports = { 15269 }
c2s_interface = "0.0.0.0"
s2s_interface = "0.0.0.0"
http_ports = { 15280 }
http_interfaces = { "0.0.0.0" }
-- Компоненты (XEP-0114) — в 13.0 сервис 'component' слушает только default 5347
-- (опция component_ports удалена из кода 13.0). Проброс: хост 15347 -> контейнер 5347.
-- ВАЖНО: (1) внешний компонент объявляется блоком Component БЕЗ модуля, иначе
-- mod_component не загрузится и порт 5347 не будет слушаться;
-- (2) component_interfaces обязана быть в ГЛОБАЛЬНОЙ секции (выше VirtualHost/Component),
-- иначе Prosody молча игнорирует её и listener висит на 127.0.0.1.
component_interfaces = { "0.0.0.0" }
Component "telegram.test.nixg.ru"
component_secret = "test-secret-telegram-123"
modules_enabled = { "privilege" } -- чтобы mod_privilege реагировал на component-authenticated
-- Тестовый домен
VirtualHost "test.nixg.ru"
authentication = "internal_hashed"
storage = "internal"
allow_registration = true -- для теста можно регистрироваться in-band
modules_enabled = { "privilege" } -- XEP-0356: модуль должен жить на VirtualHost, чтобы видеть privileged_entities
-- Привилегии для проверки mod_privilege (XEP-0356)
privileged_entities = {
["telegram.test.nixg.ru"] = {
roster = "both";
message = "outgoing";
iq = {
["http://jabber.org/protocol/pubsub"] = "both";
["http://jabber.org/protocol/pubsub#owner"] = "set";
};
};
}
-- PubSub как компонент (XEP-0060; в 13.0 модуль pubsub в VirtualHost не грузится)
Component "pubsub.test.nixg.ru" "pubsub"
modules_enabled = { "pubsub" }