diff --git a/federation/entities/activitypub/mappers.py b/federation/entities/activitypub/mappers.py index 3f5206f..58ce31d 100644 --- a/federation/entities/activitypub/mappers.py +++ b/federation/entities/activitypub/mappers.py @@ -64,6 +64,13 @@ def element_to_objects(payload: Dict) -> List: entity._receivers = extract_receivers(payload) if hasattr(entity, "post_receive"): entity.post_receive() + try: + entity.validate() + except ValueError as ex: + logger.error("Failed to validate entity %s: %s", entity, ex, extra={ + "transformed": transformed, + }) + return [] if hasattr(entity, "extract_mentions"): entity.extract_mentions() return [entity] diff --git a/federation/entities/activitypub/models.py b/federation/entities/activitypub/models.py index a46f5b3..f8298d4 100644 --- a/federation/entities/activitypub/models.py +++ b/federation/entities/activitypub/models.py @@ -233,49 +233,49 @@ class Object(metaclass=JsonLDAnnotation): @pre_load def update_context(self, data, **kwargs): if not data.get('@context'): return data - ctx = data['@context'] + ctx = copy(data['@context']) # add a # at the end of the python-federation string # for socialhome payloads s = json.dumps(ctx) if 'python-federation"' in s: ctx = json.loads(s.replace('python-federation', 'python-federation#', 1)) - data['@context'] = ctx # AP activities may be signed, but most platforms don't # define RsaSignature2017. add it to the context # hubzilla doesn't define the discoverable property in its context - to_add = {'signature': ['https://w3id.org/security/v1', {'sec':'https://w3id.org/security#','RsaSignature2017':'sec:RsaSignature2017'}], + may_add = {'signature': ['https://w3id.org/security/v1', {'sec':'https://w3id.org/security#','RsaSignature2017':'sec:RsaSignature2017'}], 'discoverable': [{'toot':'http://joinmastodon.org/ns#','discoverable': 'toot:discoverable'}], #for hubzilla 'copiedTo': [{'toot':'http://joinmastodon.org/ns#','copiedTo': 'toot:copiedTo'}], #for hubzilla 'featured': [{'toot':'http://joinmastodon.org/ns#','featured': 'toot:featured'}] #for litepub } - if not isinstance(ctx, list): - ctx = [ctx, {}] - idx = [i for i,v in enumerate(ctx) if isinstance(v, dict)] - if len(idx) == 0: - ctx.append({}) - idx = [len(ctx)-1] - saved_ctx = copy(ctx) + to_add = [val for key,val in may_add.items() if data.get(key)] + if to_add: + idx = [i for i,v in enumerate(ctx) if isinstance(v, dict)] + if idx: + upd = ctx[idx[0]] + # merge context dicts + if len(idx) > 1: + idx.reverse() + for i in idx[:-1]: + upd.update(ctx[i]) + ctx.pop(i) + else: + upd = {} - for key,val in to_add.items(): - if not data.get(key): continue - for item in val: - if isinstance(item, str) and item not in ctx: - ctx.append(item) - elif isinstance(item, dict): - for akey, aval in item.items(): - found = False - for i in idx: - if ctx[i].get(aval): - found = True - break - if not found: - ctx[idx[0]][akey] = aval - - if saved_ctx != ctx: - data['@context'] = ctx + for add in to_add: + for val in add: + if isinstance(val, str) and val not in ctx: + try: + ctx.append(val) + except AttributeError: + ctx = [ctx, val] + if isinstance(val, dict): + upd.update(val) + if not idx and upd: ctx.append(upd) + + data['@context'] = ctx return data # A node without an id isn't true json-ld, but many payloads have @@ -503,6 +503,8 @@ class Person(Object): 'large': self.icon[0].url } + entity._allowed_children += (PropertyValue, IdentityProof) + set_public(entity) return entity @@ -640,6 +642,7 @@ class Announce(Activity): target_id = IRI(as2.object) def to_base(self): + if self.activity == self: entity = ActivitypubShare(**self.__dict__) else: @@ -695,6 +698,7 @@ class Delete(Create): def to_base(self): if hasattr(self, 'object_') and not isinstance(self.object_, Tombstone): self.target_id = self.object_ + self.entity_type = 'Object' return ActivitypubRetraction(**self.__dict__) class Meta: diff --git a/federation/utils/activitypub.py b/federation/utils/activitypub.py index befa296..eeef777 100644 --- a/federation/utils/activitypub.py +++ b/federation/utils/activitypub.py @@ -4,11 +4,18 @@ from typing import Optional, Any from federation.entities.activitypub.entities import ActivitypubProfile from federation.entities.activitypub.mappers import message_to_objects +from federation.protocols.activitypub.signing import get_http_authentication from federation.utils.network import fetch_document, try_retrieve_webfinger_document from federation.utils.text import decode_if_bytes, validate_handle logger = logging.getLogger('federation') +try: + from federation.utils.django import get_admin_user + admin_user = get_admin_user() +except ImportError: + admin_user = None + logger.warning("django is required for requests signing") def get_profile_id_from_webfinger(handle: str) -> Optional[str]: """ @@ -36,7 +43,9 @@ def retrieve_and_parse_document(fid: str) -> Optional[Any]: """ Retrieve remote document by ID and return the entity. """ - document, status_code, ex = fetch_document(fid, extra_headers={'accept': 'application/activity+json'}) + document, status_code, ex = fetch_document(fid, + extra_headers={'accept': 'application/activity+json'}, + auth=get_http_authentication(admin_user.rsa_private_key,f'{admin_user.id}#main-key') if admin_user else None) if document: document = json.loads(decode_if_bytes(document)) entities = message_to_objects(document, fid) diff --git a/federation/utils/django.py b/federation/utils/django.py index 13f3925..aa05ea1 100644 --- a/federation/utils/django.py +++ b/federation/utils/django.py @@ -2,6 +2,7 @@ import importlib from django.conf import settings from django.core.exceptions import ImproperlyConfigured +from federation.types import UserType def get_configuration(): @@ -27,6 +28,7 @@ def get_configuration(): "get_private_key_function" in configuration, "get_profile_function" in configuration, "base_url" in configuration, + "federation_id" in configuration, ]): raise ImproperlyConfigured("Missing required FEDERATION settings, please check documentation.") return configuration @@ -42,3 +44,18 @@ def get_function_from_config(item): module = importlib.import_module(module_path) func = getattr(module, func_name) return func + +def get_admin_user(): + config = get_configuration() + if not config.get('federation_id'): return None + + try: + get_key = get_function_from_config("get_private_key_function") + except AttributeError: + return None + + key = get_key(config['federation_id']) + if not key: return None + + return UserType(id=config['federation_id'], private_key=key) + diff --git a/federation/utils/network.py b/federation/utils/network.py index e341969..ab84af9 100644 --- a/federation/utils/network.py +++ b/federation/utils/network.py @@ -31,7 +31,7 @@ def fetch_content_type(url: str) -> Optional[str]: return response.headers.get('Content-Type') -def fetch_document(url=None, host=None, path="/", timeout=10, raise_ssl_errors=True, extra_headers=None): +def fetch_document(url=None, host=None, path="/", timeout=10, raise_ssl_errors=True, extra_headers=None, **kwargs): """Helper method to fetch remote document. Must be given either the ``url`` or ``host``. @@ -44,6 +44,7 @@ def fetch_document(url=None, host=None, path="/", timeout=10, raise_ssl_errors=T :arg timeout: Seconds to wait for response (defaults to 10) :arg raise_ssl_errors: Pass False if you want to try HTTP even for sites with SSL errors (default True) :arg extra_headers: Optional extra headers dictionary to add to requests + :arg kwargs holds extra args passed to requests.get :returns: Tuple of document (str or None), status code (int or None) and error (an exception class instance or None) :raises ValueError: If neither url nor host are given as parameters """ @@ -59,7 +60,7 @@ def fetch_document(url=None, host=None, path="/", timeout=10, raise_ssl_errors=T # Use url since it was given logger.debug("fetch_document: trying %s", url) try: - response = requests.get(url, timeout=timeout, headers=headers) + response = requests.get(url, timeout=timeout, headers=headers, **kwargs) logger.debug("fetch_document: found document, code %s", response.status_code) response.raise_for_status() return response.text, response.status_code, None