diff --git a/src/common/caps.py b/src/common/caps_cache.py similarity index 79% rename from src/common/caps.py rename to src/common/caps_cache.py index 9706c85e9..37ff65bab 100644 --- a/src/common/caps.py +++ b/src/common/caps_cache.py @@ -1,5 +1,5 @@ # -*- coding:utf-8 -*- -## src/common/caps.py +## src/common/caps_cache.py ## ## Copyright (C) 2007 Tomasz Melcer ## Travis Shirk @@ -31,13 +31,11 @@ CapsCache caches features to hash relationships. The cache is queried through ClientCaps objects which are hold by contact instances. """ -import gajim -import helpers import base64 import hashlib import logging -log = logging.getLogger('gajim.c.caps') +log = logging.getLogger('gajim.c.caps_cache') from common.xmpp import (NS_XHTML_IM, NS_RECEIPTS, NS_ESESSION, NS_CHATSTATES, NS_JINGLE_ICE_UDP, NS_JINGLE_RTP_AUDIO, NS_JINGLE_RTP_VIDEO, NS_CAPS) @@ -75,6 +73,20 @@ def client_supports(client_caps, requested_feature): return requested_feature not in FEATURE_BLACKLIST else: return False + +def create_suitable_client_caps(node, caps_hash, hash_method): + """ + Create and return a suitable ClientCaps object for the given node, + caps_hash, hash_method combination. + """ + if not node or not caps_hash: + # improper caps, ignore client capabilities. + client_caps = NullClientCaps() + elif not hash_method: + client_caps = OldClientCaps(caps_hash, node) + else: + client_caps = ClientCaps(caps_hash, node, hash_method) + return client_caps def compute_caps_hash(identities, features, dataforms=[], hash_method='sha-1'): """ @@ -151,22 +163,6 @@ def compute_caps_hash(identities, features, dataforms=[], hash_method='sha-1'): ### Internal classes of this module ################################################################################ - -def create_suitable_client_caps(node, caps_hash, hash_method): - """ - Create and return a suitable ClientCaps object for the given node, - caps_hash, hash_method combination. - """ - if not node or not caps_hash: - # improper caps, ignore client capabilities. - client_caps = NullClientCaps() - elif not hash_method: - client_caps = OldClientCaps(caps_hash, node) - else: - client_caps = ClientCaps(caps_hash, node, hash_method) - return client_caps - - class AbstractClientCaps(object): """ Base class representing a client and its capabilities as advertised by a @@ -208,7 +204,6 @@ class ClientCaps(AbstractClientCaps): """ The current XEP-115 implementation """ - def __init__(self, caps_hash, node, hash_method): AbstractClientCaps.__init__(self, caps_hash, node) assert hash_method != 'old' @@ -230,7 +225,6 @@ class OldClientCaps(AbstractClientCaps): """ Old XEP-115 implemtation. Kept around for background competability """ - def __init__(self, caps_hash, node): AbstractClientCaps.__init__(self, caps_hash, node) @@ -251,7 +245,6 @@ class NullClientCaps(AbstractClientCaps): Assumes (almost) everything is supported. """ - def __init__(self): AbstractClientCaps.__init__(self, None, None) @@ -273,7 +266,6 @@ class CapsCache(object): This object keeps the mapping between caps data and real disco features they represent, and provides simple way to query that info """ - def __init__(self, logger=None): # our containers: # __cache is a dictionary mapping: pair of hash method and hash maps @@ -345,6 +337,13 @@ class CapsCache(object): if not self._recently_seen: self._recently_seen = True self._logger.update_caps_time(self.hash_method, self.hash) + + def is_valid(self): + """ + Returns True if identities and features for this cache item + are known. + """ + return self.status == CACHED self.__CacheItem = CacheItem self.logger = logger @@ -391,80 +390,4 @@ class CapsCache(object): else: q.update_last_seen() -################################################################################ -### Caps network coding -################################################################################ - -class ConnectionCaps(object): - - def __init__(self, account, dispatch_event): - self._account = account - self._dispatch_event = dispatch_event - - def _capsPresenceCB(self, con, presence): - """ - XMMPPY callback method to handle retrieved caps info - """ - try: - jid = helpers.get_full_jid_from_iq(presence) - except: - log.info("Ignoring invalid JID in caps presenceCB") - return - - client_caps = self._extract_client_caps_from_presence(presence) - capscache.query_client_of_jid_if_unknown(self, jid, client_caps) - self._update_client_caps_of_contact(jid, client_caps) - - self._dispatch_event('CAPS_RECEIVED', (jid,)) - - def _extract_client_caps_from_presence(self, presence): - caps_tag = presence.getTag('c', namespace=NS_CAPS) - if caps_tag: - hash_method, node, caps_hash = caps_tag['hash'], caps_tag['node'], caps_tag['ver'] - else: - hash_method = node = caps_hash = None - return create_suitable_client_caps(node, caps_hash, hash_method) - - def _update_client_caps_of_contact(self, jid, client_caps): - contact = self._get_contact_or_gc_contact_for_jid(jid) - if contact: - contact.client_caps = client_caps - else: - log.info("Received Caps from unknown contact %s" % jid) - - def _get_contact_or_gc_contact_for_jid(self, jid): - contact = gajim.contacts.get_contact_from_full_jid(self._account, jid) - if contact is None: - room_jid, nick = gajim.get_room_and_nick_from_fjid(jid) - contact = gajim.contacts.get_gc_contact(self._account, room_jid, nick) - return contact - - def _capsDiscoCB(self, jid, node, identities, features, dataforms): - """ - XMMPPY callback to update our caps cache with queried information after - we have retrieved an unknown caps hash and issued a disco - """ - contact = self._get_contact_or_gc_contact_for_jid(jid) - if not contact: - log.info("Received Disco from unknown contact %s" % jid) - return - - lookup = contact.client_caps.get_cache_lookup_strategy() - cache_item = lookup(capscache) - - if cache_item.status == CACHED: - return - else: - validate = contact.client_caps.get_hash_validation_strategy() - hash_is_valid = validate(identities, features, dataforms) - - if hash_is_valid: - cache_item.set_and_store(identities, features) - else: - contact.client_caps = NullClientCaps() - log.warn("Computed and retrieved caps hash differ." + - "Ignoring caps of contact %s" % contact.get_full_jid()) - - self._dispatch_event('CAPS_RECEIVED', (jid,)) - -# vim: se ts=3: +# vim: se ts=3: \ No newline at end of file diff --git a/src/common/connection_handlers.py b/src/common/connection_handlers.py index 76598d422..b4cc7afea 100644 --- a/src/common/connection_handlers.py +++ b/src/common/connection_handlers.py @@ -50,7 +50,8 @@ from common import exceptions from common.commands import ConnectionCommands from common.pubsub import ConnectionPubSub from common.pep import ConnectionPEP -from common.caps import ConnectionCaps +from common.protocol.caps import ConnectionCaps +import common.caps_cache as capscache if gajim.HAVE_FARSIGHT: from common.jingle import ConnectionJingle else: @@ -1520,7 +1521,8 @@ class ConnectionHandlers(ConnectionVcard, ConnectionBytestream, ConnectionPEP.__init__(self, account=self.name, dispatcher=self, pubsub_connection=self) ConnectionCaps.__init__(self, account=self.name, - dispatch_event=self.dispatch) + dispatch_event=self.dispatch, capscache=capscache.capscache, + client_caps_factory=capscache.create_suitable_client_caps) ConnectionJingle.__init__(self) ConnectionHandlersBase.__init__(self) self.gmail_url = None diff --git a/src/common/contacts.py b/src/common/contacts.py index ce1d78c43..cd3b5f01b 100644 --- a/src/common/contacts.py +++ b/src/common/contacts.py @@ -29,7 +29,7 @@ ## -from common import caps +from common import caps_cache from common.account import Account import common.gajim @@ -54,7 +54,7 @@ class CommonContact(XMPPEntity): self.status = status self.name = name - self.client_caps = client_caps or caps.NullClientCaps() + self.client_caps = client_caps or caps_cache.NullClientCaps() # please read xep-85 http://www.xmpp.org/extensions/xep-0085.html # we keep track of xep85 support with the peer by three extra states: @@ -89,7 +89,7 @@ class CommonContact(XMPPEntity): # return caps for a contact that has no resources left. return False else: - return caps.client_supports(self.client_caps, requested_feature) + return caps_cache.client_supports(self.client_caps, requested_feature) class Contact(CommonContact): diff --git a/src/common/gajim.py b/src/common/gajim.py index 7c3753a6e..cbe612b5a 100644 --- a/src/common/gajim.py +++ b/src/common/gajim.py @@ -211,8 +211,8 @@ gajim_optional_features = {} # Capabilities hash per account caps_hash = {} -import caps -caps.initialize(logger) +import caps_cache +caps_cache.initialize(logger) def get_nick_from_jid(jid): pos = jid.find('@') diff --git a/src/common/helpers.py b/src/common/helpers.py index ca590bbe7..79aff0772 100644 --- a/src/common/helpers.py +++ b/src/common/helpers.py @@ -38,7 +38,7 @@ import errno import select import base64 import hashlib -import caps +import caps_cache from encodings.punycode import punycode_encode @@ -1309,7 +1309,7 @@ def update_optional_features(account = None): gajim.gajim_optional_features[a].append(xmpp.NS_JINGLE_RTP_AUDIO) gajim.gajim_optional_features[a].append(xmpp.NS_JINGLE_RTP_VIDEO) gajim.gajim_optional_features[a].append(xmpp.NS_JINGLE_ICE_UDP) - gajim.caps_hash[a] = caps.compute_caps_hash([gajim.gajim_identity], + gajim.caps_hash[a] = caps_cache.compute_caps_hash([gajim.gajim_identity], gajim.gajim_common_features + gajim.gajim_optional_features[a]) # re-send presence with new hash connected = gajim.connections[a].connected diff --git a/src/common/optparser.py b/src/common/optparser.py index fa7b99daa..d28e26549 100644 --- a/src/common/optparser.py +++ b/src/common/optparser.py @@ -32,7 +32,7 @@ import re from time import time from common import gajim from common import helpers -from common import caps +from common import caps_cache import sqlite3 as sqlite import logger @@ -218,7 +218,7 @@ class OptionsParser: gajim.logger.init_vars() gajim.config.set('version', new_version) - caps.capscache.initialize_from_db() + caps_cache.capscache.initialize_from_db() def assert_unread_msgs_table_exists(self): """ diff --git a/src/common/protocol/__init__.py b/src/common/protocol/__init__.py new file mode 100644 index 000000000..f50b2cd01 --- /dev/null +++ b/src/common/protocol/__init__.py @@ -0,0 +1,3 @@ +""" +Implementations of specific XMPP protocols and XEPs +""" \ No newline at end of file diff --git a/src/common/protocol/caps.py b/src/common/protocol/caps.py new file mode 100644 index 000000000..2844d05ae --- /dev/null +++ b/src/common/protocol/caps.py @@ -0,0 +1,110 @@ +# -*- coding:utf-8 -*- +## src/common/protocol/caps.py +## +## Copyright (C) 2009 Stephan Erb +## +## This file is part of Gajim. +## +## Gajim is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published +## by the Free Software Foundation; version 3 only. +## +## Gajim is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with Gajim. If not, see . +## + +""" +Module containing the network portion of XEP-115 (Entity Capabilities) +""" + +import logging +log = logging.getLogger('gajim.c.p.caps') + +from common.xmpp import NS_CAPS +from common import gajim +from common import helpers + + +class ConnectionCaps(object): + + def __init__(self, account, dispatch_event, capscache, client_caps_factory): + self._account = account + self._dispatch_event = dispatch_event + self._capscache = capscache + self._create_suitable_client_caps = client_caps_factory + + def _capsPresenceCB(self, con, presence): + """ + XMMPPY callback method to handle retrieved caps info + """ + try: + jid = helpers.get_full_jid_from_iq(presence) + except: + log.info("Ignoring invalid JID in caps presenceCB") + return + + client_caps = self._extract_client_caps_from_presence(presence) + self._capscache.query_client_of_jid_if_unknown(self, jid, client_caps) + self._update_client_caps_of_contact(jid, client_caps) + + self._dispatch_event('CAPS_RECEIVED', (jid,)) + + def _extract_client_caps_from_presence(self, presence): + caps_tag = presence.getTag('c', namespace=NS_CAPS) + if caps_tag: + hash_method, node, caps_hash = caps_tag['hash'], caps_tag['node'], caps_tag['ver'] + else: + hash_method = node = caps_hash = None + return self._create_suitable_client_caps(node, caps_hash, hash_method) + + def _update_client_caps_of_contact(self, jid, client_caps): + contact = self._get_contact_or_gc_contact_for_jid(jid) + if contact: + contact.client_caps = client_caps + else: + log.info("Received Caps from unknown contact %s" % jid) + + def _get_contact_or_gc_contact_for_jid(self, jid): + contact = gajim.contacts.get_contact_from_full_jid(self._account, jid) + if contact is None: + room_jid, nick = gajim.get_room_and_nick_from_fjid(jid) + contact = gajim.contacts.get_gc_contact(self._account, room_jid, nick) + return contact + + def _capsDiscoCB(self, jid, node, identities, features, dataforms): + """ + XMMPPY callback to update our caps cache with queried information after + we have retrieved an unknown caps hash and issued a disco + """ + contact = self._get_contact_or_gc_contact_for_jid(jid) + if not contact: + log.info("Received Disco from unknown contact %s" % jid) + return + + lookup = contact.client_caps.get_cache_lookup_strategy() + cache_item = lookup(self._capscache) + + if cache_item.is_valid(): + return + else: + validate = contact.client_caps.get_hash_validation_strategy() + hash_is_valid = validate(identities, features, dataforms) + + if hash_is_valid: + cache_item.set_and_store(identities, features) + else: + node = caps_hash = hash_method = None + contact.client_caps = self._create_suitable_client_caps(node, + caps_hash, hash_method) + log.warn("Computed and retrieved caps hash differ." + + "Ignoring caps of contact %s" % contact.get_full_jid()) + + self._dispatch_event('CAPS_RECEIVED', (jid,)) + + +# vim: se ts=3: \ No newline at end of file diff --git a/src/gui_interface.py b/src/gui_interface.py index b9df817ad..fb6c25856 100644 --- a/src/gui_interface.py +++ b/src/gui_interface.py @@ -71,7 +71,7 @@ import common.sleepy from common.xmpp import idlequeue from common.zeroconf import connection_zeroconf from common import resolver -from common import caps +from common import caps_cache from common import proxy65_manager from common import socks5 from common import helpers @@ -3307,7 +3307,7 @@ class Interface: helpers.update_optional_features() # prepopulate data which we are sure of; note: we do not log these info for account in gajim.connections: - gajimcaps = caps.capscache[('sha-1', gajim.caps_hash[account])] + gajimcaps = caps_cache.capscache[('sha-1', gajim.caps_hash[account])] gajimcaps.identities = [gajim.gajim_identity] gajimcaps.features = gajim.gajim_common_features + \ gajim.gajim_optional_features[account] diff --git a/test/runtests.py b/test/runtests.py index 2f1256f7f..6bca37228 100755 --- a/test/runtests.py +++ b/test/runtests.py @@ -37,7 +37,8 @@ for o, a in opts: # new test modules need to be added manually modules = ( 'unit.test_xmpp_dispatcher_nb', 'unit.test_xmpp_transports_nb', - 'unit.test_caps', + 'unit.test_protocol_caps', + 'unit.test_caps_cache', 'unit.test_contacts', 'unit.test_sessions', 'unit.test_account', diff --git a/test/unit/test_caps.py b/test/unit/test_caps_cache.py similarity index 70% rename from test/unit/test_caps.py rename to test/unit/test_caps_cache.py index 1194c89e8..bb6069bc0 100644 --- a/test/unit/test_caps.py +++ b/test/unit/test_caps_cache.py @@ -1,14 +1,13 @@ ''' Tests for capabilities and the capabilities cache ''' -from common.caps import NullClientCaps import unittest import lib lib.setup_env() from common.xmpp import NS_MUC, NS_PING, NS_XHTML_IM -from common import caps +from common import caps_cache as caps from common.contacts import Contact from mock import Mock @@ -146,65 +145,6 @@ class TestOldClientCaps(TestClientCaps): connection.mockCheckCall(0, "discoverInfo", "test@gajim.org") - -from common.xmpp import simplexml -from common.xmpp import protocol - -class TestableConnectionCaps(caps.ConnectionCaps): - - def __init__(self, *args, **kwargs): - self._mocked_contacts = {} - caps.ConnectionCaps.__init__(self, *args, **kwargs) - - def _get_contact_or_gc_contact_for_jid(self, jid): - """ - Overwrite to decouple form contact handling - """ - if jid not in self._mocked_contacts: - self._mocked_contacts[jid] = Mock(realClass=Contact) - self._mocked_contacts[jid].jid = jid - return self._mocked_contacts[jid] - - def discoverInfo(self, *args, **kwargs): - pass - - def get_mocked_contact_for_jid(self, jid): - return self._mocked_contacts[jid] - - -class TestConnectionCaps(CommonCapsTest): - - def test_capsPresenceCB(self): - jid = "user@server.com/a" - connection_caps = TestableConnectionCaps("account", - self._build_assertering_dispatcher_function("CAPS_RECEIVED", jid)) - - xml = """ - - - """ % (jid) - iq = protocol.Iq(node=simplexml.XML2Node(xml)) - connection_caps._capsPresenceCB(None, iq) - - self.assertTrue(self._dispatcher_called, msg="Must have received caps") - - client_caps = connection_caps.get_mocked_contact_for_jid(jid).client_caps - self.assertTrue(client_caps, msg="Client caps must be set") - self.assertFalse(isinstance(client_caps, NullClientCaps), - msg="On receive of proper caps, we must not use the fallback") - - def _build_assertering_dispatcher_function(self, expected_event, jid): - self._dispatcher_called = False - def dispatch(event, data): - self.assertFalse(self._dispatcher_called, msg="Must only be called once") - self._dispatcher_called = True - self.assertEqual(expected_event, event) - self.assertEqual(jid, data[0]) - return dispatch - - if __name__ == '__main__': unittest.main() diff --git a/test/unit/test_contacts.py b/test/unit/test_contacts.py index 4cda25619..ecfe793c8 100644 --- a/test/unit/test_contacts.py +++ b/test/unit/test_contacts.py @@ -9,7 +9,7 @@ lib.setup_env() from common.contacts import CommonContact, Contact, GC_Contact, LegacyContactsAPI from common.xmpp import NS_MUC -from common import caps +from common import caps_cache class TestCommonContact(unittest.TestCase): @@ -23,11 +23,11 @@ class TestCommonContact(unittest.TestCase): Test the caps support method of contacts. See test_caps for more enhanced tests. ''' - caps.capscache = caps.CapsCache() + caps_cache.capscache = caps_cache.CapsCache() self.assertTrue(self.contact.supports(NS_MUC), msg="Must not backtrace on simple check for supported feature") - self.contact.client_caps = caps.NullClientCaps() + self.contact.client_caps = caps_cache.NullClientCaps() self.assertTrue(self.contact.supports(NS_MUC), msg="Must not backtrace on simple check for supported feature") diff --git a/test/unit/test_protocol_caps.py b/test/unit/test_protocol_caps.py new file mode 100644 index 000000000..f2c7ae509 --- /dev/null +++ b/test/unit/test_protocol_caps.py @@ -0,0 +1,75 @@ +''' +Tests for caps network coding +''' +import unittest + +import lib +lib.setup_env() + +from common import caps_cache +from common.protocol import caps +from common.contacts import Contact + +from mock import Mock + +from common.xmpp import simplexml +from common.xmpp import protocol + +class TestableConnectionCaps(caps.ConnectionCaps): + + def __init__(self, *args, **kwargs): + self._mocked_contacts = {} + caps.ConnectionCaps.__init__(self, *args, **kwargs) + + def _get_contact_or_gc_contact_for_jid(self, jid): + """ + Overwrite to decouple form contact handling + """ + if jid not in self._mocked_contacts: + self._mocked_contacts[jid] = Mock(realClass=Contact) + self._mocked_contacts[jid].jid = jid + return self._mocked_contacts[jid] + + def discoverInfo(self, *args, **kwargs): + pass + + def get_mocked_contact_for_jid(self, jid): + return self._mocked_contacts[jid] + + +class TestConnectionCaps(unittest.TestCase): + + def test_capsPresenceCB(self): + jid = "user@server.com/a" + connection_caps = TestableConnectionCaps("account", + self._build_assertering_dispatcher_function("CAPS_RECEIVED", jid), + Mock(), caps_cache.create_suitable_client_caps) + + xml = """ + + + """ % (jid) + iq = protocol.Iq(node=simplexml.XML2Node(xml)) + connection_caps._capsPresenceCB(None, iq) + + self.assertTrue(self._dispatcher_called, msg="Must have received caps") + + client_caps = connection_caps.get_mocked_contact_for_jid(jid).client_caps + self.assertTrue(client_caps, msg="Client caps must be set") + self.assertFalse(isinstance(client_caps, caps_cache.NullClientCaps), + msg="On receive of proper caps, we must not use the fallback") + + def _build_assertering_dispatcher_function(self, expected_event, jid): + self._dispatcher_called = False + def dispatch(event, data): + self.assertFalse(self._dispatcher_called, msg="Must only be called once") + self._dispatcher_called = True + self.assertEqual(expected_event, event) + self.assertEqual(jid, data[0]) + return dispatch + +if __name__ == '__main__': + unittest.main() + +# vim: se ts=3: