From c93a27e418ee552009686aae897bb17586a84570 Mon Sep 17 00:00:00 2001 From: Andrew Godwin Date: Fri, 7 Jul 2023 16:29:12 -0600 Subject: [PATCH] Capture and don't thrash on badly formatted AP messages --- activities/models/post.py | 4 +- core/exceptions.py | 6 + core/ld.py | 4 +- users/models/inbox_message.py | 268 +++++++++++++++++----------------- 4 files changed, 149 insertions(+), 133 deletions(-) diff --git a/activities/models/post.py b/activities/models/post.py index e03d8d7..7b7e3e9 100644 --- a/activities/models/post.py +++ b/activities/models/post.py @@ -25,7 +25,7 @@ from activities.models.post_types import ( PostTypeDataEncoder, QuestionData, ) -from core.exceptions import capture_message +from core.exceptions import ActivityPubFormatError, capture_message from core.html import ContentRenderer, FediverseHtmlParser from core.ld import ( canonicalise, @@ -916,6 +916,8 @@ class Post(StatorModel): focal_x, focal_y = None, None mimetype = attachment.get("mediaType") if not mimetype or not isinstance(mimetype, str): + if "url" not in attachment: + raise ActivityPubFormatError("No URL present on attachment") mimetype, _ = mimetypes.guess_type(attachment["url"]) if not mimetype: mimetype = "application/octet-stream" diff --git a/core/exceptions.py b/core/exceptions.py index 558e67f..149fb5d 100644 --- a/core/exceptions.py +++ b/core/exceptions.py @@ -9,6 +9,12 @@ class ActivityPubError(BaseException): """ +class ActivityPubFormatError(ActivityPubError): + """ + A problem with an ActivityPub message's format/keys + """ + + class ActorMismatchError(ActivityPubError): """ The actor is not authorised to do the action we saw diff --git a/core/ld.py b/core/ld.py index 6baac29..f127f74 100644 --- a/core/ld.py +++ b/core/ld.py @@ -6,6 +6,8 @@ from dateutil import parser from pyld import jsonld from pyld.jsonld import JsonLdError +from core.exceptions import ActivityPubFormatError + schemas = { "www.w3.org/ns/activitystreams": { "contentType": "application/ld+json", @@ -695,7 +697,7 @@ def get_value_or_map(data, key, map_key): if "und" in map_key: return data[map_key]["und"] return list(data[map_key].values())[0] - raise KeyError(f"Cannot find {key} or {map_key}") + raise ActivityPubFormatError(f"Cannot find {key} or {map_key}") def media_type_from_filename(filename): diff --git a/users/models/inbox_message.py b/users/models/inbox_message.py index 8500190..fe8963b 100644 --- a/users/models/inbox_message.py +++ b/users/models/inbox_message.py @@ -1,15 +1,16 @@ from django.db import models +from core.exceptions import ActivityPubError from stator.models import State, StateField, StateGraph, StatorModel class InboxMessageStates(StateGraph): received = State(try_interval=300, delete_after=86400 * 3) processed = State(externally_progressed=True, delete_after=86400) - purge = State(delete_after=24 * 60 * 60) # Delete after release (back compat) + errored = State(externally_progressed=True, delete_after=86400) received.transitions_to(processed) - processed.transitions_to(purge) # Delete after release (back compat) + received.transitions_to(errored) @classmethod def handle_received(cls, instance: "InboxMessage"): @@ -17,140 +18,145 @@ class InboxMessageStates(StateGraph): from users.models import Block, Follow, Identity, Report from users.services import IdentityService - match instance.message_type: - case "follow": - Follow.handle_request_ap(instance.message) - case "block": - Block.handle_ap(instance.message) - case "announce": - PostInteraction.handle_ap(instance.message) - case "like": - PostInteraction.handle_ap(instance.message) - case "create": - match instance.message_object_type: - case "note": - if instance.message_object_has_content: - Post.handle_create_ap(instance.message) - else: - # Notes without content are Interaction candidates - PostInteraction.handle_ap(instance.message) - case "question": - Post.handle_create_ap(instance.message) - case unknown: - if unknown in Post.Types.names: - Post.handle_create_ap(instance.message) - case "update": - match instance.message_object_type: - case "note": - Post.handle_update_ap(instance.message) - case "person": - Identity.handle_update_ap(instance.message) - case "service": - Identity.handle_update_ap(instance.message) - case "group": - Identity.handle_update_ap(instance.message) - case "organization": - Identity.handle_update_ap(instance.message) - case "application": - Identity.handle_update_ap(instance.message) - case "question": - Post.handle_update_ap(instance.message) - case unknown: - if unknown in Post.Types.names: - Post.handle_update_ap(instance.message) - case "accept": - match instance.message_object_type: - case "follow": - Follow.handle_accept_ap(instance.message) - case None: - # It's a string object, but these will only be for Follows - Follow.handle_accept_ap(instance.message) - case unknown: - raise ValueError( - f"Cannot handle activity of type accept.{unknown}" - ) - case "reject": - match instance.message_object_type: - case "follow": - Follow.handle_reject_ap(instance.message) - case None: - # It's a string object, but these will only be for Follows - Follow.handle_reject_ap(instance.message) - case unknown: - raise ValueError( - f"Cannot handle activity of type reject.{unknown}" - ) - case "undo": - match instance.message_object_type: - case "follow": - Follow.handle_undo_ap(instance.message) - case "block": - Block.handle_undo_ap(instance.message) - case "like": - PostInteraction.handle_undo_ap(instance.message) - case "announce": - PostInteraction.handle_undo_ap(instance.message) - case "http://litepub.social/ns#emojireact": - # We're ignoring emoji reactions for now - pass - case unknown: - raise ValueError( - f"Cannot handle activity of type undo.{unknown}" - ) - case "delete": - # If there is no object type, we need to see if it's a profile or a post - if not isinstance(instance.message["object"], dict): - if Identity.objects.filter( - actor_uri=instance.message["object"] - ).exists(): - Identity.handle_delete_ap(instance.message) - elif Post.objects.filter( - object_uri=instance.message["object"] - ).exists(): - Post.handle_delete_ap(instance.message) - else: - # It is presumably already deleted - pass - else: + try: + match instance.message_type: + case "follow": + Follow.handle_request_ap(instance.message) + case "block": + Block.handle_ap(instance.message) + case "announce": + PostInteraction.handle_ap(instance.message) + case "like": + PostInteraction.handle_ap(instance.message) + case "create": match instance.message_object_type: - case "tombstone": - Post.handle_delete_ap(instance.message) case "note": - Post.handle_delete_ap(instance.message) + if instance.message_object_has_content: + Post.handle_create_ap(instance.message) + else: + # Notes without content are Interaction candidates + PostInteraction.handle_ap(instance.message) + case "question": + Post.handle_create_ap(instance.message) + case unknown: + if unknown in Post.Types.names: + Post.handle_create_ap(instance.message) + case "update": + match instance.message_object_type: + case "note": + Post.handle_update_ap(instance.message) + case "person": + Identity.handle_update_ap(instance.message) + case "service": + Identity.handle_update_ap(instance.message) + case "group": + Identity.handle_update_ap(instance.message) + case "organization": + Identity.handle_update_ap(instance.message) + case "application": + Identity.handle_update_ap(instance.message) + case "question": + Post.handle_update_ap(instance.message) + case unknown: + if unknown in Post.Types.names: + Post.handle_update_ap(instance.message) + case "accept": + match instance.message_object_type: + case "follow": + Follow.handle_accept_ap(instance.message) + case None: + # It's a string object, but these will only be for Follows + Follow.handle_accept_ap(instance.message) case unknown: raise ValueError( - f"Cannot handle activity of type delete.{unknown}" + f"Cannot handle activity of type accept.{unknown}" ) - case "add": - PostInteraction.handle_add_ap(instance.message) - case "remove": - PostInteraction.handle_remove_ap(instance.message) - case "move": - # We're ignoring moves for now - pass - case "http://litepub.social/ns#emojireact": - # We're ignoring emoji reactions for now - pass - case "flag": - # Received reports - Report.handle_ap(instance.message) - case "__internal__": - match instance.message_object_type: - case "fetchpost": - Post.handle_fetch_internal(instance.message["object"]) - case "cleartimeline": - TimelineEvent.handle_clear_timeline(instance.message["object"]) - case "addfollow": - IdentityService.handle_internal_add_follow( - instance.message["object"] - ) - case unknown: - raise ValueError( - f"Cannot handle activity of type __internal__.{unknown}" - ) - case unknown: - raise ValueError(f"Cannot handle activity of type {unknown}") - return cls.processed + case "reject": + match instance.message_object_type: + case "follow": + Follow.handle_reject_ap(instance.message) + case None: + # It's a string object, but these will only be for Follows + Follow.handle_reject_ap(instance.message) + case unknown: + raise ValueError( + f"Cannot handle activity of type reject.{unknown}" + ) + case "undo": + match instance.message_object_type: + case "follow": + Follow.handle_undo_ap(instance.message) + case "block": + Block.handle_undo_ap(instance.message) + case "like": + PostInteraction.handle_undo_ap(instance.message) + case "announce": + PostInteraction.handle_undo_ap(instance.message) + case "http://litepub.social/ns#emojireact": + # We're ignoring emoji reactions for now + pass + case unknown: + raise ValueError( + f"Cannot handle activity of type undo.{unknown}" + ) + case "delete": + # If there is no object type, we need to see if it's a profile or a post + if not isinstance(instance.message["object"], dict): + if Identity.objects.filter( + actor_uri=instance.message["object"] + ).exists(): + Identity.handle_delete_ap(instance.message) + elif Post.objects.filter( + object_uri=instance.message["object"] + ).exists(): + Post.handle_delete_ap(instance.message) + else: + # It is presumably already deleted + pass + else: + match instance.message_object_type: + case "tombstone": + Post.handle_delete_ap(instance.message) + case "note": + Post.handle_delete_ap(instance.message) + case unknown: + raise ValueError( + f"Cannot handle activity of type delete.{unknown}" + ) + case "add": + PostInteraction.handle_add_ap(instance.message) + case "remove": + PostInteraction.handle_remove_ap(instance.message) + case "move": + # We're ignoring moves for now + pass + case "http://litepub.social/ns#emojireact": + # We're ignoring emoji reactions for now + pass + case "flag": + # Received reports + Report.handle_ap(instance.message) + case "__internal__": + match instance.message_object_type: + case "fetchpost": + Post.handle_fetch_internal(instance.message["object"]) + case "cleartimeline": + TimelineEvent.handle_clear_timeline( + instance.message["object"] + ) + case "addfollow": + IdentityService.handle_internal_add_follow( + instance.message["object"] + ) + case unknown: + raise ValueError( + f"Cannot handle activity of type __internal__.{unknown}" + ) + case unknown: + raise ValueError(f"Cannot handle activity of type {unknown}") + return cls.processed + except ActivityPubError: + return cls.errored class InboxMessage(StatorModel):