Welcome to mirror list, hosted at ThFree Co, Russian Federation.

dev.gajim.org/gajim/python-nbxmpp.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/nbxmpp
diff options
context:
space:
mode:
authorPhilipp Hörist <philipp@hoerist.com>2019-06-29 11:59:48 +0300
committerPhilipp Hörist <philipp@hoerist.com>2019-06-29 11:59:48 +0300
commitf57bd1aef78ee7ddb37045efe7fc81434a2c2114 (patch)
treeb151e044015161f37b3c49a12ec030f5f9944d4d /nbxmpp
parentf9532825b89abf55d6d41a0b7aa50c91dd4c9250 (diff)
Add method and tests for entity caps hash computation
Diffstat (limited to 'nbxmpp')
-rw-r--r--nbxmpp/modules/discovery.py97
-rw-r--r--nbxmpp/protocol.py3
-rw-r--r--nbxmpp/structs.py25
-rw-r--r--nbxmpp/util.py114
4 files changed, 191 insertions, 48 deletions
diff --git a/nbxmpp/modules/discovery.py b/nbxmpp/modules/discovery.py
index 8a775c6..8cb1b49 100644
--- a/nbxmpp/modules/discovery.py
+++ b/nbxmpp/modules/discovery.py
@@ -49,37 +49,7 @@ class Discovery:
def _disco_info_received(self, stanza):
if not isResultNode(stanza):
return raise_error(log.info, stanza)
-
- idenities = []
- features = []
- dataforms = []
-
- query = stanza.getQuery()
- for node in query.getTags('identity'):
- attrs = node.getAttrs()
- try:
- idenities.append(
- DiscoIdentity(category=attrs['category'],
- type=attrs['type'],
- name=attrs.get('name'),
- lang=attrs.get('xml:lang')))
- except Exception:
- return raise_error(log.warning, stanza, 'stanza-malformed')
-
- for node in query.getTags('feature'):
- try:
- features.append(node.getAttr('var'))
- except Exception:
- return raise_error(log.warning, stanza, 'stanza-malformed')
-
- for node in query.getTags('x', namespace=NS_DATA):
- dataforms.append(extend_form(node))
-
- return DiscoInfo(jid=stanza.getFrom(),
- node=query.getAttr('node'),
- identities=idenities,
- features=features,
- dataforms=dataforms)
+ return parse_disco_info(stanza)
@call_on_response('_disco_items_received')
def disco_items(self, jid, node=None):
@@ -90,24 +60,59 @@ class Discovery:
def _disco_items_received(self, stanza):
if not isResultNode(stanza):
return raise_error(log.info, stanza)
+ return parse_disco_items(stanza)
- items = []
-
- query = stanza.getQuery()
- for node in query.getTags('item'):
- attrs = node.getAttrs()
- try:
- items.append(
- DiscoItem(jid=attrs['jid'],
- name=attrs.get('name'),
- node=attrs.get('node')))
- except Exception:
- return raise_error(log.warning, stanza, 'stanza-malformed')
- return DiscoItems(jid=stanza.getFrom(),
- node=query.getAttr('node'),
- items=items)
+def parse_disco_info(stanza):
+ idenities = []
+ features = []
+ dataforms = []
+ query = stanza.getQuery()
+ for node in query.getTags('identity'):
+ attrs = node.getAttrs()
+ try:
+ idenities.append(
+ DiscoIdentity(category=attrs['category'],
+ type=attrs['type'],
+ name=attrs.get('name'),
+ lang=attrs.get('xml:lang')))
+ except Exception:
+ return raise_error(log.warning, stanza, 'stanza-malformed')
+
+ for node in query.getTags('feature'):
+ try:
+ features.append(node.getAttr('var'))
+ except Exception:
+ return raise_error(log.warning, stanza, 'stanza-malformed')
+
+ for node in query.getTags('x', namespace=NS_DATA):
+ dataforms.append(extend_form(node))
+
+ return DiscoInfo(jid=stanza.getFrom(),
+ node=query.getAttr('node'),
+ identities=idenities,
+ features=features,
+ dataforms=dataforms)
+
+
+def parse_disco_items(stanza):
+ items = []
+
+ query = stanza.getQuery()
+ for node in query.getTags('item'):
+ attrs = node.getAttrs()
+ try:
+ items.append(
+ DiscoItem(jid=attrs['jid'],
+ name=attrs.get('name'),
+ node=attrs.get('node')))
+ except Exception:
+ return raise_error(log.warning, stanza, 'stanza-malformed')
+
+ return DiscoItems(jid=stanza.getFrom(),
+ node=query.getAttr('node'),
+ items=items)
def get_disco_request(namespace, jid, node=None):
diff --git a/nbxmpp/protocol.py b/nbxmpp/protocol.py
index b292799..c502dd7 100644
--- a/nbxmpp/protocol.py
+++ b/nbxmpp/protocol.py
@@ -664,6 +664,9 @@ class InvalidJid(Exception):
class StanzaMalformed(Exception):
pass
+class DiscoInfoMalformed(Exception):
+ pass
+
stream_exceptions = {'bad-format': BadFormat,
'bad-namespace-prefix': BadNamespacePrefix,
'conflict': Conflict,
diff --git a/nbxmpp/structs.py b/nbxmpp/structs.py
index c33f422..6f7ac29 100644
--- a/nbxmpp/structs.py
+++ b/nbxmpp/structs.py
@@ -113,14 +113,35 @@ IBBData = namedtuple('IBBData', 'block_size sid seq type data')
IBBData.__new__.__defaults__ = (None, None, None, None, None)
DiscoInfo = namedtuple('DiscoInfo', 'jid node identities features dataforms')
-DiscoIdentity = namedtuple('DiscoIdentity', 'category type name lang')
-DiscoIdentity.__new__.__defaults__ = (None, None)
DiscoItems = namedtuple('DiscoItems', 'jid node items')
DiscoItem = namedtuple('DiscoItem', 'jid name node')
DiscoItem.__new__.__defaults__ = (None, None)
+class DiscoIdentity(namedtuple('DiscoIdentity', 'category type name lang')):
+
+ __slots__ = []
+
+ def __new__(cls, category, type, name=None, lang=None):
+ return super(DiscoIdentity, cls).__new__(cls, category, type, name, lang)
+
+ def __eq__(self, other):
+ return str(self) == str(other)
+
+ def __ne__(self, other):
+ return not self.__eq__(other)
+
+ def __str__(self):
+ return '%s/%s/%s/%s' % (self.category,
+ self.type,
+ self.lang or '',
+ self.name or '')
+
+ def __hash__(self):
+ return hash(str(self))
+
+
class AdHocCommand(namedtuple('AdHocCommand', 'jid node name sessionid status data actions notes')):
__slots__ = []
diff --git a/nbxmpp/util.py b/nbxmpp/util.py
index f088c64..79b5060 100644
--- a/nbxmpp/util.py
+++ b/nbxmpp/util.py
@@ -27,6 +27,7 @@ import precis_i18n.codec
from nbxmpp.protocol import JID
from nbxmpp.protocol import InvalidJid
+from nbxmpp.protocol import DiscoInfoMalformed
from nbxmpp.stringprepare import nameprep
from nbxmpp.structs import Properties
from nbxmpp.structs import IqProperties
@@ -254,3 +255,116 @@ def text_to_color(text, background_color):
bc = 0.2 * bb_inv + 0.8 * blue
return rc, gc, bc
+
+
+def compute_caps_hash(info):
+ """
+ Compute caps hash according to XEP-0115, V1.5
+ https://xmpp.org/extensions/xep-0115.html#ver-proc
+
+ :param: info DiscoInfo
+ """
+ # Initialize an empty string S.
+ string_ = ''
+
+ # Sort the service discovery identities by category and then by type and
+ # then by xml:lang (if it exists), formatted as
+ # CATEGORY '/' [TYPE] '/' [LANG] '/' [NAME]. Note that each slash is
+ # included even if the LANG or NAME is not included (in accordance with
+ # XEP-0030, the category and type MUST be included).
+ # For each identity, append the 'category/type/lang/name' to S, followed by
+ # the '<' character.
+ # Sort the supported service discovery features.
+
+ def sort_identities_key(i):
+ return (i.category, i.type, i.lang or '')
+
+ identities = sorted(info.identities, key=sort_identities_key)
+ for identity in identities:
+ string_ += '%s<' % str(identity)
+
+ # If the response includes more than one service discovery identity with
+ # the same category/type/lang/name, consider the entire response
+ # to be ill-formed.
+ if len(set(identities)) != len(identities):
+ raise DiscoInfoMalformed('Non-unique identity found')
+
+ # Sort the supported service discovery features.
+ # For each feature, append the feature to S, followed by the '<' character.
+ features = sorted(info.features)
+ for feature in features:
+ string_ += '%s<' % feature
+
+ # If the response includes more than one service discovery feature with the
+ # same XML character data, consider the entire response to be ill-formed.
+ if len(set(features)) != len(features):
+ raise DiscoInfoMalformed('Non-unique feature found')
+
+ # If the response includes more than one extended service discovery
+ # information form with the same FORM_TYPE or the FORM_TYPE field contains
+ # more than one <value/> element with different XML character data,
+ # consider the entire response to be ill-formed.
+
+ # If the response includes an extended service discovery information form
+ # where the FORM_TYPE field is not of type "hidden" or the form does not
+ # include a FORM_TYPE field, ignore the form but continue processing.
+
+ dataforms = []
+ form_type_values = []
+ for dataform in info.dataforms:
+ form_type = dataform.vars.get('FORM_TYPE')
+ if form_type is None:
+ # Ignore dataform because of missing FORM_TYPE
+ continue
+ if form_type.type_ != 'hidden':
+ # Ignore dataform because of wrong type
+ continue
+
+ values = form_type.getTags('value')
+ if len(values) != 1:
+ raise DiscoInfoMalformed('Form should have exactly '
+ 'one FORM_TYPE value')
+ value = values[0].getData()
+
+ dataforms.append(dataform)
+ form_type_values.append(value)
+
+ if len(set(form_type_values)) != len(form_type_values):
+ raise DiscoInfoMalformed('Non-unique FORM_TYPE value found')
+
+ # If the service discovery information response includes XEP-0128 data
+ # forms, sort the forms by the FORM_TYPE (i.e., by the XML character data
+ # of the <value/> element).
+
+ # For each extended service discovery information form:
+ # - Append the XML character data of the FORM_TYPE field's <value/>
+ # element, followed by the '<' character.
+ # - Sort the fields by the value of the "var" attribute.
+ # - For each field other than FORM_TYPE:
+ # - Append the value of the "var" attribute, followed by the
+ # '<' character.
+ # - Sort values by the XML character data of the <value/> element.
+ # - For each <value/> element, append the XML character data,
+ # followed by the '<' character.
+
+ def sort_dataforms_key(dataform):
+ return dataform['FORM_TYPE'].getTagData('value')
+
+ dataforms = sorted(dataforms, key=sort_dataforms_key)
+ for dataform in dataforms:
+ string_ += '%s<' % dataform['FORM_TYPE'].getTagData('value')
+
+ fields = {}
+ for field in dataform.iter_fields():
+ if field.var == 'FORM_TYPE':
+ continue
+ values = field.getTags('value')
+ fields[field.var] = sorted([value.getData() for value in values])
+
+ for var in sorted(fields.keys()):
+ string_ += '%s<' % var
+ for value in fields[var]:
+ string_ += '%s<' % value
+
+ hash_ = hashlib.sha1(string_.encode())
+ return b64encode(hash_.digest())