Basic checks for inbox

This commit is contained in:
Mouse Reeve 2021-02-15 16:26:48 -08:00
parent 9cbecec8ac
commit fd19b55961
5 changed files with 195 additions and 14 deletions

View file

@ -160,14 +160,12 @@ def handle_follow_accept(activity):
accepter = activitypub.resolve_remote_id(models.User, activity['actor'])
try:
request = models.UserFollowRequest.objects.get(
models.UserFollowRequest.objects.get(
user_subject=requester,
user_object=accepter
)
request.delete()
).accept()
except models.UserFollowRequest.DoesNotExist:
pass
accepter.followers.add(requester)
return
@app.task
@ -176,12 +174,13 @@ def handle_follow_reject(activity):
requester = models.User.objects.get(remote_id=activity['object']['actor'])
rejecter = activitypub.resolve_remote_id(models.User, activity['actor'])
request = models.UserFollowRequest.objects.get(
user_subject=requester,
user_object=rejecter
)
request.delete()
#raises models.UserFollowRequest.DoesNotExist
try:
models.UserFollowRequest.objects.get(
user_subject=requester,
user_object=rejecter
).reject()
except models.UserFollowRequest.DoesNotExist:
return
@app.task
def handle_block(activity):

View file

@ -0,0 +1,102 @@
''' tests incoming activities'''
import json
from unittest.mock import patch
from django.http import HttpResponseNotAllowed, HttpResponseNotFound
from django.test import TestCase, Client
from bookwyrm import models
class Inbox(TestCase):
''' readthrough tests '''
def setUp(self):
''' basic user and book data '''
self.client = Client()
self.local_user = models.User.objects.create_user(
'mouse@example.com', 'mouse@mouse.com', 'mouseword',
local=True, localname='mouse')
self.local_user.remote_id = 'https://example.com/user/mouse'
self.local_user.save(broadcast=False)
with patch('bookwyrm.models.user.set_remote_server.delay'):
self.remote_user = models.User.objects.create_user(
'rat', 'rat@rat.com', 'ratword',
local=False,
remote_id='https://example.com/users/rat',
inbox='https://example.com/users/rat/inbox',
outbox='https://example.com/users/rat/outbox',
)
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'):
self.status = models.Status.objects.create(
user=self.local_user,
content='Test status',
remote_id='https://example.com/status/1',
)
models.SiteSettings.objects.create()
def test_inbox_invalid_get(self):
''' shouldn't try to handle if the user is not found '''
result = self.client.get(
'/inbox', content_type="application/json"
)
self.assertIsInstance(result, HttpResponseNotAllowed)
def test_inbox_invalid_user(self):
''' shouldn't try to handle if the user is not found '''
result = self.client.post(
'/user/bleh/inbox',
'{"type": "Test", "object": "exists"}',
content_type="application/json"
)
self.assertIsInstance(result, HttpResponseNotFound)
def test_inbox_invalid_bad_signature(self):
''' bad request for invalid signature '''
with patch('bookwyrm.views.inbox.has_valid_signature') as mock_valid:
mock_valid.return_value = False
result = self.client.post(
'/user/mouse/inbox',
'{"type": "Test", "object": "exists"}',
content_type="application/json"
)
self.assertEqual(result.status_code, 401)
def test_inbox_invalid_bad_signature_delete(self):
''' invalid signature for Delete is okay though '''
with patch('bookwyrm.views.inbox.has_valid_signature') as mock_valid:
mock_valid.return_value = False
result = self.client.post(
'/user/mouse/inbox',
'{"type": "Delete", "object": "exists"}',
content_type="application/json"
)
self.assertEqual(result.status_code, 200)
def test_inbox_unknown_type(self):
''' never heard of that activity type, don't have a handler for it '''
with patch('bookwyrm.views.inbox.has_valid_signature') as mock_valid:
result = self.client.post(
'/inbox',
'{"type": "Fish", "object": "exists"}',
content_type="application/json"
)
mock_valid.return_value = True
self.assertIsInstance(result, HttpResponseNotFound)
def test_inbox_success(self):
''' a known type, for which we start a task '''
activity = {
"id": "hi",
"type": "Accept",
"actor": "https://example.com/users/rat",
"object": "https://example.com/user/mouse"
}
with patch('bookwyrm.views.inbox.has_valid_signature') as mock_valid:
mock_valid.return_value = True
result = self.client.post(
'/inbox',
json.dumps(activity),
content_type="application/json"
)
self.assertEqual(result.status_code, 200)

View file

@ -4,7 +4,7 @@ from django.contrib import admin
from django.urls import path, re_path
from bookwyrm import incoming, settings, views, wellknown
from bookwyrm import settings, views, wellknown
from bookwyrm.utils import regex
user_path = r'^user/(?P<username>%s)' % regex.username
@ -29,8 +29,8 @@ urlpatterns = [
path('admin/', admin.site.urls),
# federation endpoints
re_path(r'^inbox/?$', incoming.shared_inbox),
re_path(r'%s/inbox/?$' % local_user_path, incoming.inbox),
re_path(r'^inbox/?$', views.Inbox.as_view()),
re_path(r'%s/inbox/?$' % local_user_path, views.Inbox.as_view()),
re_path(r'%s/outbox/?$' % local_user_path, views.Outbox.as_view()),
re_path(r'^.well-known/webfinger/?$', wellknown.webfinger),
re_path(r'^.well-known/nodeinfo/?$', wellknown.nodeinfo_pointer),

View file

@ -11,6 +11,7 @@ from .follow import follow, unfollow
from .follow import accept_follow_request, delete_follow_request
from .goal import Goal
from .import_data import Import, ImportStatus
from .inbox import Inbox
from .interaction import Favorite, Unfavorite, Boost, Unboost
from .invite import ManageInvites, Invite
from .landing import About, Home, Discover

79
bookwyrm/views/inbox.py Normal file
View file

@ -0,0 +1,79 @@
''' incoming activities '''
import json
from urllib.parse import urldefrag
from django.http import HttpResponse
from django.http import HttpResponseBadRequest, HttpResponseNotFound
from django.shortcuts import get_object_or_404
from django.views import View
import requests
from bookwyrm import activitypub, models
from bookwyrm.signatures import Signature
# 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 let's do some basic checks to see if this is legible
if username:
try:
models.User.objects.get(localname=username)
except models.User.DoesNotExist:
return HttpResponseNotFound()
# is it valid json? does it at least vaguely resemble an activity?
try:
resp = request.body
activity_json = json.loads(resp)
activity_type = activity_json['type'] # Follow, Accept, Create, etc
except (json.decoder.JSONDecodeError, KeyError):
return HttpResponseBadRequest()
# 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)
# get the activity dataclass from the type field
try:
serializer = getattr(activitypub, activity_type)
serializer(**activity_json)
except (AttributeError, activitypub.ActivitySerializerError):
return HttpResponseNotFound()
return HttpResponse()
def has_valid_signature(request, activity):
''' verify incoming signature '''
try:
signature = Signature.parse(request)
key_actor = urldefrag(signature.key_id).url
if key_actor != activity.get('actor'):
raise ValueError("Wrong actor created signature.")
remote_user = activitypub.resolve_remote_id(models.User, key_actor)
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(
models.User, remote_user.remote_id, 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