Don't waste DB rows on bad inbox actors

Seems Sidekiq will keep trying to deliver messages even when the actor
no longer exists?
This commit is contained in:
Andrew Godwin 2022-11-20 14:20:28 -07:00
parent 70d01bf1b4
commit 6e88c00969
5 changed files with 59 additions and 20 deletions

View file

@ -1,3 +1,9 @@
import traceback
from asgiref.sync import sync_to_async
from django.conf import settings
class ActivityPubError(BaseException): class ActivityPubError(BaseException):
""" """
A problem with an ActivityPub message A problem with an ActivityPub message
@ -8,3 +14,30 @@ class ActorMismatchError(ActivityPubError):
""" """
The actor is not authorised to do the action we saw The actor is not authorised to do the action we saw
""" """
def capture_message(message: str):
"""
Sends the informational message to Sentry if it's configured
"""
if settings.SENTRY_ENABLED:
from sentry_sdk import capture_message
capture_message(message)
elif settings.DEBUG:
print(message)
def capture_exception(exception: BaseException):
"""
Sends the exception to Sentry if it's configured
"""
if settings.SENTRY_ENABLED:
from sentry_sdk import capture_exception
capture_exception(exception)
elif settings.DEBUG:
traceback.print_exc()
acapture_exception = sync_to_async(capture_exception, thread_sensitive=False)

View file

@ -4,11 +4,11 @@ import traceback
from typing import ClassVar, List, Optional, Type, Union, cast from typing import ClassVar, List, Optional, Type, Union, cast
from asgiref.sync import sync_to_async from asgiref.sync import sync_to_async
from django.conf import settings
from django.db import models, transaction from django.db import models, transaction
from django.utils import timezone from django.utils import timezone
from django.utils.functional import classproperty from django.utils.functional import classproperty
from core import exceptions
from stator.graph import State, StateGraph from stator.graph import State, StateGraph
@ -155,10 +155,7 @@ class StatorModel(models.Model):
next_state = await current_state.handler(self) next_state = await current_state.handler(self)
except BaseException as e: except BaseException as e:
await StatorError.acreate_from_instance(self, e) await StatorError.acreate_from_instance(self, e)
if settings.SENTRY_ENABLED: await exceptions.acapture_exception(e)
from sentry_sdk import capture_exception
await sync_to_async(capture_exception, thread_sensitive=False)(e)
traceback.print_exc() traceback.print_exc()
else: else:
if next_state: if next_state:

View file

@ -5,10 +5,9 @@ import traceback
import uuid import uuid
from typing import List, Optional, Type from typing import List, Optional, Type
from asgiref.sync import sync_to_async
from django.conf import settings
from django.utils import timezone from django.utils import timezone
from core import exceptions
from stator.models import StatorModel from stator.models import StatorModel
@ -93,10 +92,7 @@ class StatorRunner:
) )
await instance.atransition_attempt() await instance.atransition_attempt()
except BaseException as e: except BaseException as e:
if settings.SENTRY_ENABLED: await exceptions.acapture_exception(e)
from sentry_sdk import capture_exception
await sync_to_async(capture_exception, thread_sensitive=False)(e)
traceback.print_exc() traceback.print_exc()
def remove_completed_tasks(self): def remove_completed_tasks(self):

View file

@ -176,11 +176,16 @@ class Identity(StatorModel):
return None return None
@classmethod @classmethod
def by_actor_uri(cls, uri, create=False) -> "Identity": def by_actor_uri(cls, uri, create=False, transient=False) -> "Identity":
try: try:
return cls.objects.get(actor_uri=uri) return cls.objects.get(actor_uri=uri)
except cls.DoesNotExist: except cls.DoesNotExist:
if create: if create:
if transient:
# Some code (like inbox fetching) doesn't need this saved
# to the DB until the fetch succeeds
return cls(actor_uri=uri, local=False)
else:
return cls.objects.create(actor_uri=uri, local=False) return cls.objects.create(actor_uri=uri, local=False)
else: else:
raise cls.DoesNotExist(f"No identity found with actor_uri {uri}") raise cls.DoesNotExist(f"No identity found with actor_uri {uri}")
@ -329,6 +334,7 @@ class Identity(StatorModel):
return False return False
if response.status_code == 410: if response.status_code == 410:
# Their account got deleted, so let's do the same. # Their account got deleted, so let's do the same.
if self.pk:
await Identity.objects.filter(pk=self.pk).adelete() await Identity.objects.filter(pk=self.pk).adelete()
return False return False
if response.status_code >= 400: if response.status_code >= 400:

View file

@ -8,6 +8,7 @@ from django.views.decorators.csrf import csrf_exempt
from django.views.generic import View from django.views.generic import View
from activities.models import Post from activities.models import Post
from core import exceptions
from core.ld import canonicalise from core.ld import canonicalise
from core.models import Config from core.models import Config
from core.signatures import ( from core.signatures import (
@ -131,22 +132,26 @@ class Inbox(View):
# Find the Identity by the actor on the incoming item # Find the Identity by the actor on the incoming item
# This ensures that the signature used for the headers matches the actor # This ensures that the signature used for the headers matches the actor
# described in the payload. # described in the payload.
identity = Identity.by_actor_uri(document["actor"], create=True) identity = Identity.by_actor_uri(document["actor"], create=True, transient=True)
if not identity.public_key: if not identity.public_key:
# See if we can fetch it right now # See if we can fetch it right now
async_to_sync(identity.fetch_actor)() async_to_sync(identity.fetch_actor)()
if not identity.public_key: if not identity.public_key:
print("Cannot get actor", document["actor"]) exceptions.capture_message(
f"Inbox error: cannot fetch actor {document['actor']}"
)
return HttpResponseBadRequest("Cannot retrieve actor") return HttpResponseBadRequest("Cannot retrieve actor")
# If there's a "signature" payload, verify against that # If there's a "signature" payload, verify against that
if "signature" in document: if "signature" in document:
try: try:
LDSignature.verify_signature(document, identity.public_key) LDSignature.verify_signature(document, identity.public_key)
except VerificationFormatError as e: except VerificationFormatError as e:
print("Bad LD signature format:", e.args[0]) exceptions.capture_message(
f"Inbox error: Bad LD signature format: {e.args[0]}"
)
return HttpResponseBadRequest(e.args[0]) return HttpResponseBadRequest(e.args[0])
except VerificationError: except VerificationError:
print("Bad LD signature") exceptions.capture_message("Inbox error: Bad LD signature")
return HttpResponseUnauthorized("Bad signature") return HttpResponseUnauthorized("Bad signature")
# Otherwise, verify against the header (assuming it's the same actor) # Otherwise, verify against the header (assuming it's the same actor)
else: else:
@ -156,10 +161,12 @@ class Inbox(View):
identity.public_key, identity.public_key,
) )
except VerificationFormatError as e: except VerificationFormatError as e:
print("Bad HTTP signature format:", e.args[0]) exceptions.capture_message(
f"Inbox error: Bad HTTP signature format: {e.args[0]}"
)
return HttpResponseBadRequest(e.args[0]) return HttpResponseBadRequest(e.args[0])
except VerificationError: except VerificationError:
print("Bad HTTP signature") exceptions.capture_message("Inbox error: Bad HTTP signature")
return HttpResponseUnauthorized("Bad signature") return HttpResponseUnauthorized("Bad signature")
# Hand off the item to be processed by the queue # Hand off the item to be processed by the queue
InboxMessage.objects.create(message=document) InboxMessage.objects.create(message=document)