176 lines
4.7 KiB
Python
176 lines
4.7 KiB
Python
########################################################################
|
|
# File name: caps115.py
|
|
# This file is part of: aioxmpp
|
|
#
|
|
# LICENSE
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU Lesser General Public License as
|
|
# published by the Free Software Foundation, either version 3 of the
|
|
# License, or (at your option) any later version.
|
|
#
|
|
# This program 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
|
|
# Lesser General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU Lesser General Public
|
|
# License along with this program. If not, see
|
|
# <http://www.gnu.org/licenses/>.
|
|
#
|
|
########################################################################
|
|
import base64
|
|
import collections
|
|
import hashlib
|
|
import pathlib
|
|
import urllib.parse
|
|
|
|
from xml.sax.saxutils import escape
|
|
|
|
from .common import AbstractKey, AbstractImplementation
|
|
from . import xso as caps_xso
|
|
|
|
|
|
def build_identities_string(identities):
|
|
identities = [
|
|
b"/".join([
|
|
escape(identity.category).encode("utf-8"),
|
|
escape(identity.type_).encode("utf-8"),
|
|
escape(str(identity.lang or "")).encode("utf-8"),
|
|
escape(identity.name or "").encode("utf-8"),
|
|
])
|
|
for identity in identities
|
|
]
|
|
|
|
if len(set(identities)) != len(identities):
|
|
raise ValueError("duplicate identity")
|
|
|
|
identities.sort()
|
|
identities.append(b"")
|
|
return b"<".join(identities)
|
|
|
|
|
|
def build_features_string(features):
|
|
features = list(escape(feature).encode("utf-8") for feature in features)
|
|
|
|
if len(set(features)) != len(features):
|
|
raise ValueError("duplicate feature")
|
|
|
|
features.sort()
|
|
features.append(b"")
|
|
return b"<".join(features)
|
|
|
|
|
|
def build_forms_string(forms):
|
|
types = set()
|
|
forms_list = []
|
|
for form in forms:
|
|
try:
|
|
form_types = set(
|
|
value
|
|
for field in form.fields.filter(attrs={"var": "FORM_TYPE"})
|
|
for value in field.values
|
|
)
|
|
except KeyError:
|
|
continue
|
|
|
|
if len(form_types) > 1:
|
|
raise ValueError("form with multiple types")
|
|
elif not form_types:
|
|
continue
|
|
|
|
type_ = escape(next(iter(form_types))).encode("utf-8")
|
|
if type_ in types:
|
|
raise ValueError("multiple forms of type {!r}".format(type_))
|
|
types.add(type_)
|
|
forms_list.append((type_, form))
|
|
forms_list.sort()
|
|
|
|
parts = []
|
|
|
|
for type_, form in forms_list:
|
|
parts.append(type_)
|
|
|
|
field_list = sorted(
|
|
(
|
|
(escape(field.var).encode("utf-8"), field.values)
|
|
for field in form.fields
|
|
if field.var != "FORM_TYPE"
|
|
),
|
|
key=lambda x: x[0]
|
|
)
|
|
|
|
for var, values in field_list:
|
|
parts.append(var)
|
|
parts.extend(sorted(
|
|
escape(value).encode("utf-8") for value in values
|
|
))
|
|
|
|
parts.append(b"")
|
|
return b"<".join(parts)
|
|
|
|
|
|
def hash_query(query, algo):
|
|
hashimpl = hashlib.new(algo)
|
|
hashimpl.update(
|
|
build_identities_string(query.identities)
|
|
)
|
|
hashimpl.update(
|
|
build_features_string(query.features)
|
|
)
|
|
hashimpl.update(
|
|
build_forms_string(query.exts)
|
|
)
|
|
|
|
return base64.b64encode(hashimpl.digest()).decode("ascii")
|
|
|
|
|
|
Key = collections.namedtuple("Key", ["algo", "node"])
|
|
|
|
|
|
class Key(Key, AbstractKey):
|
|
@property
|
|
def path(self):
|
|
quoted = urllib.parse.quote(self.node, safe="")
|
|
return (pathlib.Path("hashes") /
|
|
"{}_{}.xml".format(self.algo, quoted))
|
|
|
|
@property
|
|
def ver(self):
|
|
return self.node.rsplit("#", 1)[1]
|
|
|
|
def verify(self, query_response):
|
|
digest_b64 = hash_query(query_response, self.algo.replace("-", ""))
|
|
return self.ver == digest_b64
|
|
|
|
|
|
class Implementation(AbstractImplementation):
|
|
def __init__(self, node, **kwargs):
|
|
super().__init__(**kwargs)
|
|
self.__node = node
|
|
|
|
def extract_keys(self, obj):
|
|
caps = obj.xep0115_caps
|
|
if caps is None or caps.hash_ is None:
|
|
return
|
|
|
|
yield Key(caps.hash_, "{}#{}".format(caps.node, caps.ver))
|
|
|
|
def put_keys(self, keys, presence):
|
|
key, = keys
|
|
|
|
presence.xep0115_caps = caps_xso.Caps115(
|
|
self.__node,
|
|
key.ver,
|
|
key.algo,
|
|
)
|
|
|
|
def calculate_keys(self, query_response):
|
|
yield Key(
|
|
"sha-1",
|
|
"{}#{}".format(
|
|
self.__node,
|
|
hash_query(query_response, "sha1"),
|
|
)
|
|
)
|