Files
rust-xmpp/tests/preauth_network.py
T
2026-10-20 15:59:55 +07:00

74 lines
5.7 KiB
Python

#!/usr/bin/env python3
"""Isolated TLS contact invitation, token forwarding and roster regression."""
import json, socket, subprocess, tempfile, time, sys
from pathlib import Path
from urllib.parse import urlsplit
import xml.etree.ElementTree as ET
from _wire_client import Client, local, BIN
COMMANDS='http://jabber.org/protocol/commands'
DISCO='http://jabber.org/protocol/disco#items'
PARS='urn:xmpp:pars:0'
def roster(client, identifier):
client.send(f"<iq type='get' id='{identifier}'><query xmlns='jabber:iq:roster'/></iq>")
return {e.get('jid'):e.get('subscription') for e in client.iq(identifier).iter() if e.tag=='{jabber:iq:roster}item'}
def run():
with tempfile.TemporaryDirectory(prefix='xmpp-preauth-') as tmp:
root=Path(tmp)
subprocess.run(['openssl','req','-x509','-newkey','rsa:2048','-nodes','-days','1','-keyout',str(root/'key.pem'),'-out',str(root/'cert.pem'),'-subj','/CN=localhost','-addext','subjectAltName=DNS:localhost'],check=True,stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
with socket.socket() as probe:
probe.bind(('127.0.0.1',0));port=probe.getsockname()[1]
cfg={'global':{'data_path':str(root/'data'),'interfaces':['127.0.0.1'],'c2s_ports':[port],'http_ports':[],'https_ports':[],'s2s_ports':[],'modules_enabled':['tls','saslauth','roster','disco','pep','cloud_notify','mam','ping','smacks']},'hosts':{'localhost':{'options':{'authentication':'internal_hashed','password_hash':'SHA-256','ssl':{'key':str(root/'key.pem'),'certificate':str(root/'cert.pem')}}}}}
config=root/'config.json';config.write_text(json.dumps(cfg))
for user in ['alice','bob','carol']:
subprocess.run([str(BIN),'create-user',str(config),user,'--password-stdin'],input=user+'-secret\n',text=True,check=True,capture_output=True)
with (root/'server.log').open('w+') as log:
server=subprocess.Popen([str(BIN),'serve',str(config),'--experimental'],stdout=log,stderr=log);clients=[]
try:
for _ in range(100):
assert server.poll() is None,'server stopped'
try:
with socket.create_connection(('127.0.0.1',port),timeout=.1):break
except OSError:time.sleep(.05)
a,b,c=[Client(port,root/'cert.pem',u,u+'-secret') for u in ['alice','bob','carol']];clients=[a,b,c]
for client in clients:
features=ET.fromstring(client.post_auth_features[client.post_auth_features.index(b'<stream:features'):].replace(b'<stream:features',b"<stream:features xmlns:stream='http://etherx.jabber.org/streams'",1))
assert features.find('{urn:xmpp:features:pre-approval}sub') is not None
client.available();roster(client,'initial-roster')
a.send(f"<iq type='get' id='commands' to='localhost'><query xmlns='{DISCO}' node='{COMMANDS}'/></iq>")
assert any(n.get('node')=='urn:xmpp:invite#invite' for n in a.iq('commands').iter())
a.send(f"<iq type='set' id='invite' to='localhost'><command xmlns='{COMMANDS}' node='urn:xmpp:invite#invite' action='execute'/></iq>")
result=a.iq('invite');fields={n.get('var'):n.find('{jabber:x:data}value').text for n in result.iter('{jabber:x:data}field')}
uri=fields['uri'];assert uri.startswith('xmpp:alice@localhost?roster;preauth=') and 'ibr=y' not in uri
token=uri.split('preauth=',1)[1];assert len(token)==32 and fields['expire']
b.send("<presence to='alice@localhost' type='subscribed'/>");b.sync()
b.send(f"<presence to='alice@localhost' type='subscribe'><preauth xmlns='{PARS}' token='{token}'/></presence>");b.sync();a.sync()
assert roster(a,'alice-after').get('bob@localhost')=='both'
assert roster(b,'bob-after').get('alice@localhost')=='both'
c.send(f"<presence to='alice@localhost' type='subscribe' id='replay'><preauth xmlns='{PARS}' token='{token}'/></presence>");c.sync();a.sync()
assert roster(a,'replay-denied').get('carol@localhost')=='none'
replay=a.take(lambda n:local(n)=='presence' and n.get('id')=='replay')
assert replay.find('{'+PARS+'}preauth').get('token')==token
# Unknown client-generated tokens must reach the owner even after offline replay.
a.close();clients.remove(a);time.sleep(.1)
c.send(f"<presence to='alice@localhost' type='subscribe' id='manual'><preauth xmlns='{PARS}' token='client-generated-token'/></presence>");c.sync()
a=Client(port,root/'cert.pem','alice','alice-secret','again');clients.append(a);a.available()
pending=a.take(lambda n:local(n)=='presence' and n.get('id')=='manual')
assert pending.find('{'+PARS+'}preauth').get('token')=='client-generated-token'
# A different authenticated account cannot issue invitations in Alice's name.
c.send(f"<iq type='set' id='foreign' to='alice@localhost'><command xmlns='{COMMANDS}' node='urn:xmpp:invite#invite'/></iq>");c.iq('foreign',error=True)
print('PASS: TLS invitation command discovery, preapproval, single-use mutual roster, replay denial, unknown preauth forwarding/offline pending, account isolation')
except BaseException:
log.flush();log.seek(0);print(log.read());raise
finally:
for client in clients:client.close()
server.terminate()
try:server.wait(timeout=5)
except subprocess.TimeoutExpired:server.kill();server.wait()
if __name__=='__main__':
if len(sys.argv)>1:BIN=Path(sys.argv[1]).resolve()
run()