diff --git a/cdk/lambda/receive_inbound_activity/index.py b/cdk/lambda/receive_inbound_activity/index.py index 4668541..3d28b80 100644 --- a/cdk/lambda/receive_inbound_activity/index.py +++ b/cdk/lambda/receive_inbound_activity/index.py @@ -16,9 +16,9 @@ import json import logging import os -from typing import Any, Optional +from typing import Any, Optional, Tuple import boto3 -from libactivitypub.activity import Activity +from libactivitypub.activity import Activity, Delete from libactivitypub.actor import Actor from libactivitypub.signature import ( VerificationError, @@ -42,6 +42,8 @@ logging.getLogger('libactivitypub').setLevel(logging.DEBUG) logging.getLogger('libmumble').setLevel(logging.DEBUG) +PREFILTER_BODY_SIZE = 10 * 1024 # 10 KB + DOMAIN_NAME = get_domain_name(boto3.client('ssm')) # user table @@ -111,6 +113,30 @@ def quarantine(tag: str, payload: Any, options: Optional[Any]=None): LOGGER.debug('quarantined payload: %s', res) +def prefilter_activity(body: str) -> Tuple[Optional[str], Optional[Activity]]: + """Prefilters a given activity. + + Patterns of a returned value: + * (``None``, ``None``): activity is too large to prefilter + * (``None``, non-``None``): activity is not prefiltered + * (non-``None``, non-``None``): activity is prefiltered + + :raises ValueError: if ``body`` is not a valid ``Activity``. + + :raises TypeError: if ``body`` is not a valid ``Activity``. + """ + if len(body) > PREFILTER_BODY_SIZE: + return None, None + LOGGER.debug('prefiltering activity') + activity = Activity.parse_object(json.loads(body)) + # prefilters "Delete" activities of the actor itself + if activity.type == 'Delete': + delete = activity.cast(Delete) + if delete.actor_id == delete.object_id: + return 'Delete of the actor itself', activity + return None, activity + + def lambda_handler(event, _context): """Runs on AWS Lambda. @@ -149,6 +175,16 @@ def lambda_handler(event, _context): username = event['username'] LOGGER.debug('processing activity sent to: %s', username) + # parses activity for prefilter unless it is too large + try: + filter_tag, activity = prefilter_activity(event['body']) + if filter_tag: + LOGGER.debug('prefiltered activity: %s', filter_tag) + return + except (TypeError, ValueError) as exc: + quarantine('invalid_activity', event) + raise BadRequestError(f'{exc}') from exc + LOGGER.debug('parsing signature') try: signature = parse_signature(event['signature']) @@ -198,12 +234,13 @@ def lambda_handler(event, _context): raise UnauthorizedError(f'failed to authenticate: {exc}') from exc # once the signature is verified, we can parse the body - LOGGER.debug('parsing activity') - try: - activity = Activity.parse_object(json.loads(body)) - except ValueError as exc: - quarantine('invalid_activity', event) - raise BadRequestError(f'{exc}') from exc + if activity is None: + LOGGER.debug('parsing activity') + try: + activity = Activity.parse_object(json.loads(body)) + except (TypeError, ValueError) as exc: + quarantine('invalid_activity', event) + raise BadRequestError(f'{exc}') from exc if signer.id != activity.actor_id: quarantine('invalid_activity', event, activity.to_dict()) raise UnauthorizedError( diff --git a/lib/libactivitypub/src/libactivitypub/activity.py b/lib/libactivitypub/src/libactivitypub/activity.py index 43645a4..4da4b35 100644 --- a/lib/libactivitypub/src/libactivitypub/activity.py +++ b/lib/libactivitypub/src/libactivitypub/activity.py @@ -56,6 +56,8 @@ def parse_object(obj: Dict[str, Any]) -> 'Activity': return Announce.parse_object(obj) if obj_type == 'Create': return Create.parse_object(obj) + if obj_type == 'Delete': + return Delete.parse_object(obj) if obj_type == 'Follow': return Follow.parse_object(obj) if obj_type == 'Like': @@ -286,6 +288,52 @@ def resolve_objects(self, object_store: ObjectStore): resolve_object(self._underlying['object'], object_store) +class Delete(Activity): + """Wraps a "Delete" activity. + """ + def __init__(self, underlying: Dict[str, Any]): + """Wraps a given "Follow" activity object. + + :raises ValueError: if ``underlying`` does not represent an activity. + + :raises TypeError: if ``underlying`` does not represent a valid object, + or if ``underlying`` does not have ``object``. + """ + super().__init__(underlying) + if 'object' not in underlying: + raise TypeError('invalid Delete activity: object is missing') + + @staticmethod + def parse_object(obj: Dict[str, Any]) -> 'Delete': + """Parses a given "Delete" activity object. + + :raises ValueError: if ``obj`` does not represent an activity. + + :raises TypeError: if ``obj`` does not represent a "Delete" activity + object. + """ + if obj.get('type') != 'Delete': + raise TypeError(f'type must be "Delete": {obj.get("type")}') + return Delete(obj) + + @property + def object_id(self) -> str: + """ID of the (to be) deleted object. + """ + return Reference(self._underlying['object']).id + + def visit(self, visitor: 'ActivityVisitor'): + visitor.visit_delete(self) + + def resolve_objects(self, object_store: ObjectStore): + """Resolves the referenced object. + + Resolves ``object``. + """ + super().resolve_objects(object_store) + resolve_object(self._underlying['object'], object_store) + + class Follow(Activity): """Wraps a "Follow" activity. """ @@ -514,6 +562,11 @@ def visit_create(self, create: Create): """ LOGGER.debug('ignoring "Create": %s', create._underlying) # pylint: disable=protected-access + def visit_delete(self, delete: Delete): + """Processes a "Delete" activity. + """ + LOGGER.debug('ignoring "Delete": %s', delete._underlying) # pylint: disable=protected-access + def visit_follow(self, follow: Follow): """Processes a "Follow" activity. """