diff options
author | Philipp Hörist <philipp@hoerist.com> | 2019-06-29 11:59:48 +0300 |
---|---|---|
committer | Philipp Hörist <philipp@hoerist.com> | 2019-06-29 11:59:48 +0300 |
commit | f57bd1aef78ee7ddb37045efe7fc81434a2c2114 (patch) | |
tree | b151e044015161f37b3c49a12ec030f5f9944d4d /nbxmpp | |
parent | f9532825b89abf55d6d41a0b7aa50c91dd4c9250 (diff) |
Add method and tests for entity caps hash computation
Diffstat (limited to 'nbxmpp')
-rw-r--r-- | nbxmpp/modules/discovery.py | 97 | ||||
-rw-r--r-- | nbxmpp/protocol.py | 3 | ||||
-rw-r--r-- | nbxmpp/structs.py | 25 | ||||
-rw-r--r-- | nbxmpp/util.py | 114 |
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()) |