#!/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"")
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'")
assert any(n.get('node')=='urn:xmpp:invite#invite' for n in a.iq('commands').iter())
a.send(f"")
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("");b.sync()
b.send(f"");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"");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"");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"");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()