Skip to content
This repository has been archived by the owner on Jan 14, 2024. It is now read-only.

Commit

Permalink
entitycaps: support for XEP-0390
Browse files Browse the repository at this point in the history
  • Loading branch information
horazont committed May 1, 2017
1 parent 6032f59 commit 1a90936
Show file tree
Hide file tree
Showing 3 changed files with 613 additions and 100 deletions.
96 changes: 72 additions & 24 deletions aioxmpp/entitycaps/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,7 @@

from aioxmpp.utils import namespaces

from . import xso as my_xso
from . import caps115
from . import caps115, caps390


logger = logging.getLogger("aioxmpp.entitycaps")
Expand Down Expand Up @@ -244,18 +243,39 @@ class EntityCapsService(aioxmpp.service.Service):

on_ver_changed = aioxmpp.callbacks.Signal()

xep115_support = disco.register_feature(namespaces.xep0115_caps)
_xep115_feature = disco.register_feature(namespaces.xep0115_caps)
_xep390_feature = disco.register_feature(namespaces.xep0390_caps)

def __init__(self, node, **kwargs):
super().__init__(node, **kwargs)

self.__current_keys = None
self.__current_keys = {}
self._cache = Cache()

self.disco_server = self.dependencies[disco.DiscoServer]
self.disco_client = self.dependencies[disco.DiscoClient]

self.__115 = caps115.Implementation(self.NODE)
self.__390 = caps390.Implementation({
"sha-256",
"sha-512",
})

@property
def xep115_support(self):
return self._xep115_feature.enabled

@xep115_support.setter
def xep115_support(self, value):
self._xep115_feature.enabled = value

@property
def xep390_support(self):
return self._xep390_feature.enabled

@xep390_support.setter
def xep390_support(self, value):
self._xep390_feature.enabled = value

@property
def cache(self):
Expand Down Expand Up @@ -287,8 +307,8 @@ def _info_changed(self):

@asyncio.coroutine
def _shutdown(self):
if self.__current_keys:
for key in self.__current_keys:
for group in self.__current_keys.values():
for key in group:
self.disco_server.unmount_node(key.node)

@asyncio.coroutine
Expand Down Expand Up @@ -332,20 +352,34 @@ def lookup_info(self, jid, keys):

@aioxmpp.service.outbound_presence_filter
def handle_outbound_presence(self, presence):
if (presence.type_ == aioxmpp.structs.PresenceType.AVAILABLE and
self.__current_keys):
if self.xep115_support.enabled:
self.logger.debug(
"injecting capabilities into outbound presence"
)
self.__115.put_keys(self.__current_keys, presence)
if presence.type_ == aioxmpp.structs.PresenceType.AVAILABLE:
try:
keys = self.__current_keys[self.__115]
except KeyError:
pass
else:
self.__115.put_keys(keys, presence)

try:
keys = self.__current_keys[self.__390]
except KeyError:
pass
else:
self.__390.put_keys(keys, presence)

return presence

@aioxmpp.service.inbound_presence_filter
def handle_inbound_presence(self, presence):
if self.xep115_support.enabled:
keys = list(self.__115.extract_keys(presence))
keys = []

if self.xep390_support:
keys.extend(self.__390.extract_keys(presence))

if self.xep115_support:
keys.extend(self.__115.extract_keys(presence))

if keys:
lookup_task = asyncio.async(
self.lookup_info(presence.from_, keys)
)
Expand Down Expand Up @@ -373,18 +407,32 @@ def update_hash(self):
features=self.disco_server.iter_features(),
)

current_keys = frozenset(self.__115.calculate_keys(info))
new_keys = {}

if self.xep115_support:
new_keys[self.__115] = set(self.__115.calculate_keys(info))

self.logger.debug("new keys=%r", current_keys)
if self.xep390_support:
new_keys[self.__390] = set(self.__390.calculate_keys(info))

if self.__current_keys != current_keys:
if self.__current_keys:
for key in self.__current_keys:
self.disco_server.unmount_node(key.node)
self.__current_keys = current_keys
for key in current_keys:
if self.__current_keys == new_keys:
self.logger.debug("keys remained unchanged (%r)",
new_keys)
return

self.logger.debug("new keys=%r", new_keys)

for group in self.__current_keys.values():
for key in group:
self.disco_server.unmount_node(key.node)

self.__current_keys = new_keys

for group in new_keys.values():
for key in group:
self.disco_server.mount_node(key.node, self.disco_server)
self.on_ver_changed()

self.on_ver_changed()


def writeback(base_path, hash_, node, captured_events):
Expand Down
99 changes: 97 additions & 2 deletions tests/entitycaps/test_e2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
#
########################################################################
import asyncio
import base64

import aioxmpp.stream
import aioxmpp.entitycaps as caps
Expand Down Expand Up @@ -56,7 +57,6 @@ def setUp(self):
@blocking_timed
@asyncio.coroutine
def test_caps_are_sent_with_presence(self):
caps_server = self.source.summon(aioxmpp.EntityCapsService)
disco_client = self.sink.summon(aioxmpp.DiscoClient)
disco_server = self.source.summon(aioxmpp.DiscoServer)

Expand Down Expand Up @@ -84,6 +84,11 @@ def on_available(full_jid, stanza):
presence.xep0115_caps,
)

self.assertIsNotNone(
presence.xep0390_caps,
)
self.assertTrue(presence.xep0390_caps.digests)

info = yield from disco_client.query_info(
self.source.local_jid,
node="{}#{}".format(
Expand All @@ -96,9 +101,23 @@ def on_available(full_jid, stanza):
set(disco_server.iter_features()),
)

for algo, digest in presence.xep0390_caps.digests.items():
info = yield from disco_client.query_info(
self.source.local_jid,
node="urn:xmpp:caps#{}.{}".format(
algo,
base64.b64encode(digest).decode("ascii")
)
)
self.assertSetEqual(
info.features,
set(disco_server.iter_features()),
(algo, digest)
)

@blocking_timed
@asyncio.coroutine
def test_caps_are_processed_when_received(self):
def test_caps115_are_processed_when_received(self):
info = disco.xso.InfoQuery()
info.features.add("http://feature.test/feature")
info.features.add("http://feature.test/another-feature")
Expand Down Expand Up @@ -169,3 +188,79 @@ def fake_disco_handler(iq):
self.assertEqual(info2.features, info.features)

self.assertFalse(disco_called_again)


@blocking_timed
@asyncio.coroutine
def test_caps390_are_processed_when_received(self):
info = disco.xso.InfoQuery()
info.features.add("http://feature.test/feature")
info.features.add("http://feature.test/another-feature")

info_hash = caps.caps390._calculate_hash(
"sha-256",
caps.caps390._get_hash_input(info)
)
info_hash_b64 = base64.b64encode(info_hash).decode("ascii")

disco_called = asyncio.Future()
disco_called_again = False

@asyncio.coroutine
def fake_disco_handler(iq):
nonlocal disco_called_again
if not disco_called.done():
disco_called.set_result(iq)
else:
disco_called_again = True
return info

self.source, self.sink = yield from asyncio.gather(
self.provisioner.get_connected_client(
services=[
aioxmpp.PresenceServer,
]
),
self.provisioner.get_connected_client(
services=[
aioxmpp.EntityCapsService,
aioxmpp.PresenceServer,
aioxmpp.PresenceClient,
]
)
)

self.source.stream.register_iq_request_coro(
aioxmpp.IQType.GET,
disco.xso.InfoQuery,
fake_disco_handler,
)

presence = aioxmpp.Presence(
type_=aioxmpp.PresenceType.AVAILABLE,
to=self.sink.local_jid,
)
presence.xep0390_caps = caps.xso.Caps390()
presence.xep0390_caps.digests["sha-256"] = info_hash

yield from self.source.stream.send(presence)

info_request = (yield from disco_called).payload
self.assertEqual(
info_request.node,
"urn:xmpp:caps#sha-256.{}".format(info_hash_b64)
)

disco_client = self.sink.summon(aioxmpp.DiscoClient)
info1 = yield from disco_client.query_info(
self.source.local_jid,
node="urn:xmpp:caps#sha-256.{}".format(info_hash_b64),
)
self.assertEqual(info1.features, info.features)

info2 = yield from disco_client.query_info(
self.source.local_jid,
)
self.assertEqual(info2.features, info.features)

self.assertFalse(disco_called_again)
Loading

0 comments on commit 1a90936

Please sign in to comment.