moviewyrm/bookwyrm/views/inbox.py

138 lines
4.6 KiB
Python
Raw Normal View History

2021-03-08 16:49:10 +00:00
""" incoming activities """
2021-02-16 00:26:48 +00:00
import json
2021-04-05 22:16:21 +00:00
import re
2022-01-18 00:06:25 +00:00
import logging
from urllib.parse import urldefrag
2022-01-18 00:06:25 +00:00
import requests
2021-02-16 00:26:48 +00:00
2021-09-28 00:27:17 +00:00
from django.http import HttpResponse, Http404
from django.core.exceptions import BadRequest, PermissionDenied
from django.shortcuts import get_object_or_404
2021-02-17 02:07:57 +00:00
from django.utils.decorators import method_decorator
2021-02-16 00:26:48 +00:00
from django.views import View
2021-02-17 02:07:57 +00:00
from django.views.decorators.csrf import csrf_exempt
2021-02-16 00:26:48 +00:00
from bookwyrm import activitypub, models
2021-02-16 02:47:08 +00:00
from bookwyrm.tasks import app
2021-02-16 00:26:48 +00:00
from bookwyrm.signatures import Signature
2021-04-05 22:16:21 +00:00
from bookwyrm.utils import regex
2021-02-16 00:26:48 +00:00
2022-01-10 06:45:14 +00:00
logger = logging.getLogger(__name__)
2021-02-16 00:26:48 +00:00
2022-01-10 07:53:23 +00:00
2021-03-08 16:49:10 +00:00
@method_decorator(csrf_exempt, name="dispatch")
2021-02-16 00:26:48 +00:00
# pylint: disable=no-self-use
class Inbox(View):
2021-04-26 16:15:42 +00:00
"""requests sent by outside servers"""
2021-03-08 16:49:10 +00:00
2021-02-16 00:26:48 +00:00
def post(self, request, username=None):
2021-04-26 16:15:42 +00:00
"""only works as POST request"""
2021-04-05 22:16:21 +00:00
# first check if this server is on our shitlist
2021-09-28 00:27:17 +00:00
raise_is_blocked_user_agent(request)
2021-04-05 22:16:21 +00:00
# make sure the user's inbox even exists
2021-02-16 00:26:48 +00:00
if username:
2021-09-28 00:27:17 +00:00
get_object_or_404(models.User, localname=username, is_active=True)
2021-02-16 00:26:48 +00:00
# is it valid json? does it at least vaguely resemble an activity?
try:
2021-02-16 01:23:17 +00:00
activity_json = json.loads(request.body)
except json.decoder.JSONDecodeError:
2021-09-28 00:27:17 +00:00
raise BadRequest()
2021-02-16 00:26:48 +00:00
2021-04-05 22:16:21 +00:00
# let's be extra sure we didn't block this domain
2021-09-28 00:27:17 +00:00
raise_is_blocked_activity(activity_json)
2021-04-05 22:16:21 +00:00
2021-03-08 16:49:10 +00:00
if (
not "object" in activity_json
or not "type" in activity_json
or not activity_json["type"] in activitypub.activity_objects
):
2021-09-28 00:27:17 +00:00
raise Http404()
2021-02-16 00:26:48 +00:00
# verify the signature
if not has_valid_signature(request, activity_json):
2021-03-08 16:49:10 +00:00
if activity_json["type"] == "Delete":
2021-02-16 00:26:48 +00:00
# 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)
2021-02-16 03:41:22 +00:00
activity_task.delay(activity_json)
2021-02-16 00:26:48 +00:00
return HttpResponse()
2021-09-28 00:27:17 +00:00
def raise_is_blocked_user_agent(request):
2021-04-26 16:15:42 +00:00
"""check if a request is from a blocked server based on user agent"""
2021-04-05 22:16:21 +00:00
# check user agent
user_agent = request.headers.get("User-Agent")
2021-04-05 22:54:33 +00:00
if not user_agent:
2021-09-28 00:27:17 +00:00
return
2021-09-18 18:32:00 +00:00
url = re.search(rf"https?://{regex.DOMAIN}/?", user_agent)
2021-04-14 01:04:54 +00:00
if not url:
2021-09-28 00:27:17 +00:00
return
2021-04-14 01:26:54 +00:00
url = url.group()
2021-09-28 00:27:17 +00:00
if models.FederatedServer.is_blocked(url):
logger.debug("%s is blocked, denying request based on user agent", url)
2021-09-28 00:27:17 +00:00
raise PermissionDenied()
2021-04-05 22:16:21 +00:00
2021-09-28 00:27:17 +00:00
def raise_is_blocked_activity(activity_json):
2021-04-26 16:15:42 +00:00
"""get the sender out of activity json and check if it's blocked"""
2021-04-05 22:16:21 +00:00
actor = activity_json.get("actor")
2022-01-10 06:45:14 +00:00
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)
2021-09-28 00:27:17 +00:00
raise PermissionDenied()
2021-09-28 00:27:17 +00:00
if models.FederatedServer.is_blocked(actor):
logger.debug("%s is blocked, denying request based on actor", actor)
2021-09-28 00:27:17 +00:00
raise PermissionDenied()
2021-04-05 22:16:21 +00:00
2021-09-07 23:33:43 +00:00
@app.task(queue="medium_priority")
2021-02-16 02:47:08 +00:00
def activity_task(activity_json):
2021-04-26 16:15:42 +00:00
"""do something with this json we think is legit"""
2021-02-16 02:47:08 +00:00
# lets see if the activitypub module can make sense of this json
2021-04-08 21:15:58 +00:00
activity = activitypub.parse(activity_json)
2021-02-16 02:47:08 +00:00
# cool that worked, now we should do the action described by the type
# (create, update, delete, etc)
2021-04-08 21:15:58 +00:00
activity.action()
2021-02-16 02:47:08 +00:00
2021-02-16 00:26:48 +00:00
def has_valid_signature(request, activity):
2021-04-26 16:15:42 +00:00
"""verify incoming signature"""
2021-02-16 00:26:48 +00:00
try:
signature = Signature.parse(request)
key_actor = urldefrag(signature.key_id).url
2021-03-08 16:49:10 +00:00
if key_actor != activity.get("actor"):
2021-02-16 00:26:48 +00:00
raise ValueError("Wrong actor created signature.")
2021-03-08 16:49:10 +00:00
remote_user = activitypub.resolve_remote_id(key_actor, model=models.User)
2021-02-16 00:26:48 +00:00
if not remote_user:
return False
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
2021-02-16 00:26:48 +00:00
)
if remote_user.key_pair.public_key == old_key:
2021-03-08 16:49:10 +00:00
raise # Key unchanged.
2021-02-16 00:26:48 +00:00
signature.verify(remote_user.key_pair.public_key, request)
except (ValueError, requests.exceptions.HTTPError):
return False
return True