mirror of
https://github.com/bookwyrm-social/bookwyrm.git
synced 2025-02-19 20:36:20 +00:00
152 lines
5.2 KiB
Python
152 lines
5.2 KiB
Python
""" incoming activities """
|
|
import json
|
|
import re
|
|
import logging
|
|
|
|
import requests
|
|
|
|
from django.http import HttpResponse, Http404
|
|
from django.core.exceptions import BadRequest, PermissionDenied
|
|
from django.shortcuts import get_object_or_404
|
|
from django.utils.decorators import method_decorator
|
|
from django.views import View
|
|
from django.views.decorators.csrf import csrf_exempt
|
|
|
|
from bookwyrm import activitypub, models
|
|
from bookwyrm.tasks import app, INBOX
|
|
from bookwyrm.signatures import Signature
|
|
from bookwyrm.utils import regex
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@method_decorator(csrf_exempt, name="dispatch")
|
|
# pylint: disable=no-self-use
|
|
class Inbox(View):
|
|
"""requests sent by outside servers"""
|
|
|
|
def post(self, request, username=None):
|
|
"""only works as POST request"""
|
|
# first check if this server is on our shitlist
|
|
raise_is_blocked_user_agent(request)
|
|
|
|
# make sure the user's inbox even exists
|
|
if username:
|
|
get_object_or_404(models.User, localname=username, is_active=True)
|
|
|
|
# is it valid json? does it at least vaguely resemble an activity?
|
|
try:
|
|
activity_json = json.loads(request.body)
|
|
except json.decoder.JSONDecodeError:
|
|
raise BadRequest()
|
|
|
|
# let's be extra sure we didn't block this domain
|
|
raise_is_blocked_activity(activity_json)
|
|
|
|
if (
|
|
not "object" in activity_json
|
|
or not "type" in activity_json
|
|
or not activity_json["type"] in activitypub.activity_objects
|
|
):
|
|
raise Http404()
|
|
|
|
# verify the signature
|
|
if not has_valid_signature(request, activity_json):
|
|
if activity_json["type"] == "Delete":
|
|
# Pretend that unauth'd deletes succeed. Auth may be failing
|
|
# because the resource or owner of the resource might have
|
|
# been deleted.
|
|
return HttpResponse()
|
|
return HttpResponse(status=401)
|
|
|
|
sometimes_async_activity_task(activity_json)
|
|
return HttpResponse()
|
|
|
|
|
|
def raise_is_blocked_user_agent(request):
|
|
"""check if a request is from a blocked server based on user agent"""
|
|
# check user agent
|
|
user_agent = request.headers.get("User-Agent")
|
|
if not user_agent:
|
|
return
|
|
url = re.search(rf"https?://{regex.DOMAIN}/?", user_agent)
|
|
if not url:
|
|
return
|
|
url = url.group()
|
|
if models.FederatedServer.is_blocked(url):
|
|
logger.debug("%s is blocked, denying request based on user agent", url)
|
|
raise PermissionDenied()
|
|
|
|
|
|
def raise_is_blocked_activity(activity_json):
|
|
"""get the sender out of activity json and check if it's blocked"""
|
|
actor = activity_json.get("actor")
|
|
|
|
if not actor:
|
|
# well I guess it's not even a valid activity so who knows
|
|
return
|
|
|
|
# check if the user is banned/deleted
|
|
existing = models.User.find_existing_by_remote_id(actor)
|
|
if existing and existing.deleted:
|
|
logger.debug("%s is banned/deleted, denying request based on actor", actor)
|
|
raise PermissionDenied()
|
|
|
|
if models.FederatedServer.is_blocked(actor):
|
|
logger.debug("%s is blocked, denying request based on actor", actor)
|
|
raise PermissionDenied()
|
|
|
|
|
|
def sometimes_async_activity_task(activity_json):
|
|
"""Sometimes we can effectively respond to a request without queuing a new task,
|
|
and whenever that is possible, we should do it."""
|
|
activity = activitypub.parse(activity_json)
|
|
|
|
# try resolving this activity without making any http requests
|
|
try:
|
|
activity.action(allow_external_connections=False)
|
|
except activitypub.ActivitySerializerError:
|
|
# if that doesn't work, run it asynchronously
|
|
activity_task.apply_async(args=(activity_json,))
|
|
|
|
|
|
@app.task(queue=INBOX)
|
|
def activity_task(activity_json):
|
|
"""do something with this json we think is legit"""
|
|
# lets see if the activitypub module can make sense of this json
|
|
activity = activitypub.parse(activity_json)
|
|
|
|
# cool that worked, now we should do the action described by the type
|
|
# (create, update, delete, etc)
|
|
activity.action()
|
|
|
|
|
|
def has_valid_signature(request, activity):
|
|
"""verify incoming signature"""
|
|
try:
|
|
signature = Signature.parse(request)
|
|
remote_user = activitypub.resolve_remote_id(
|
|
activity.get("actor"), model=models.User
|
|
)
|
|
if not remote_user:
|
|
return False
|
|
|
|
if signature.key_id != remote_user.key_pair.remote_id:
|
|
if (
|
|
signature.key_id != f"{remote_user.remote_id}#main-key"
|
|
): # legacy Bookwyrm
|
|
raise ValueError("Wrong actor created signature.")
|
|
|
|
try:
|
|
signature.verify(remote_user.key_pair.public_key, request)
|
|
except ValueError:
|
|
old_key = remote_user.key_pair.public_key
|
|
remote_user = activitypub.resolve_remote_id(
|
|
remote_user.remote_id, model=models.User, refresh=True
|
|
)
|
|
if remote_user.key_pair.public_key == old_key:
|
|
raise # Key unchanged.
|
|
signature.verify(remote_user.key_pair.public_key, request)
|
|
except (ValueError, requests.exceptions.HTTPError):
|
|
return False
|
|
return True
|