From 75710638de34d0b268683d8a8722027d32a67d8f Mon Sep 17 00:00:00 2001 From: Eliot Berriot <contact@eliotberriot.com> Date: Thu, 29 Mar 2018 00:00:47 +0200 Subject: [PATCH] Url and views for instance actor and webfinger --- api/config/urls.py | 3 + api/funkwhale_api/federation/serializers.py | 21 ++++++ api/funkwhale_api/federation/urls.py | 15 +++++ api/funkwhale_api/federation/views.py | 73 +++++++++++++++++++++ api/tests/federation/test_views.py | 69 +++++++++++++++++++ 5 files changed, 181 insertions(+) create mode 100644 api/funkwhale_api/federation/serializers.py create mode 100644 api/funkwhale_api/federation/urls.py create mode 100644 api/funkwhale_api/federation/views.py create mode 100644 api/tests/federation/test_views.py diff --git a/api/config/urls.py b/api/config/urls.py index 8f7e37bc..90598ea8 100644 --- a/api/config/urls.py +++ b/api/config/urls.py @@ -13,6 +13,9 @@ urlpatterns = [ url(settings.ADMIN_URL, admin.site.urls), url(r'^api/', include(("config.api_urls", 'api'), namespace="api")), + url(r'^', include( + ('funkwhale_api.federation.urls', 'federation'), + namespace="federation")), url(r'^api/v1/auth/', include('rest_auth.urls')), url(r'^api/v1/auth/registration/', include('funkwhale_api.users.rest_auth_urls')), url(r'^accounts/', include('allauth.urls')), diff --git a/api/funkwhale_api/federation/serializers.py b/api/funkwhale_api/federation/serializers.py new file mode 100644 index 00000000..62a4500d --- /dev/null +++ b/api/funkwhale_api/federation/serializers.py @@ -0,0 +1,21 @@ +from django.urls import reverse +from django.conf import settings + +from . import utils + + +def repr_instance_actor(): + """ + We do not use a serializer here, since it's pretty static + """ + return { + '@context': [ + 'https://www.w3.org/ns/activitystreams', + 'https://w3id.org/security/v1', + {}, + ], + 'id': utils.full_url(reverse('federation:instance-actor')), + 'type': 'Service', + 'inbox': utils.full_url(reverse('federation:instance-inbox')), + 'outbox': utils.full_url(reverse('federation:instance-outbox')), + } diff --git a/api/funkwhale_api/federation/urls.py b/api/funkwhale_api/federation/urls.py new file mode 100644 index 00000000..0c59a141 --- /dev/null +++ b/api/funkwhale_api/federation/urls.py @@ -0,0 +1,15 @@ +from rest_framework import routers + +from . import views + +router = routers.SimpleRouter(trailing_slash=False) +router.register( + r'instance', + views.InstanceViewSet, + 'instance') +router.register( + r'.well-known', + views.WellKnownViewSet, + 'well-known') + +urlpatterns = router.urls diff --git a/api/funkwhale_api/federation/views.py b/api/funkwhale_api/federation/views.py new file mode 100644 index 00000000..93ce3973 --- /dev/null +++ b/api/funkwhale_api/federation/views.py @@ -0,0 +1,73 @@ +from django import forms +from django.conf import settings +from django.http import HttpResponse + +from rest_framework import viewsets +from rest_framework import views +from rest_framework import response +from rest_framework.decorators import list_route + +from . import serializers +from . import webfinger + + +class FederationMixin(object): + def dispatch(self, request, *args, **kwargs): + if not settings.FEDERATION_ENABLED: + return HttpResponse(status=405) + return super().dispatch(request, *args, **kwargs) + + +class InstanceViewSet(FederationMixin, viewsets.GenericViewSet): + authentication_classes = [] + permission_classes = [] + + @list_route(methods=['get']) + def actor(self, request, *args, **kwargs): + return response.Response(serializers.repr_instance_actor()) + + @list_route(methods=['get']) + def inbox(self, request, *args, **kwargs): + raise NotImplementedError() + + @list_route(methods=['get']) + def outbox(self, request, *args, **kwargs): + raise NotImplementedError() + + +class WellKnownViewSet(FederationMixin, viewsets.GenericViewSet): + authentication_classes = [] + permission_classes = [] + + @list_route(methods=['get']) + def webfinger(self, request, *args, **kwargs): + try: + resource_type, resource = webfinger.clean_resource( + request.GET['resource']) + cleaner = getattr(webfinger, 'clean_{}'.format(resource_type)) + result = cleaner(resource) + except forms.ValidationError as e: + return response.Response({ + 'errors': { + 'resource': e.message + } + }, status=400) + except KeyError: + return response.Response({ + 'errors': { + 'resource': 'This field is required', + } + }, status=400) + + handler = getattr(self, 'handler_{}'.format(resource_type)) + data = handler(result) + + return response.Response( + data, + content_type='application/jrd+json; charset=utf-8') + + def handler_acct(self, clean_result): + username, hostname = clean_result + if username == 'service': + return webfinger.serialize_system_acct() + return {} diff --git a/api/tests/federation/test_views.py b/api/tests/federation/test_views.py new file mode 100644 index 00000000..2bb35f8e --- /dev/null +++ b/api/tests/federation/test_views.py @@ -0,0 +1,69 @@ +from django.urls import reverse + +import pytest + +from funkwhale_api.federation import webfinger + + +def test_instance_actor(db, settings, api_client): + settings.FUNKWHALE_URL = 'http://test.com' + url = reverse('federation:instance-actor') + response = api_client.get(url) + assert response.data['id'] == ( + settings.FUNKWHALE_URL + url + ) + assert response.data['type'] == 'Service' + assert response.data['inbox'] == ( + settings.FUNKWHALE_URL + reverse('federation:instance-inbox') + ) + assert response.data['outbox'] == ( + settings.FUNKWHALE_URL + reverse('federation:instance-outbox') + ) + assert response.data['@context'] == [ + 'https://www.w3.org/ns/activitystreams', + 'https://w3id.org/security/v1', + {}, + ] + + +@pytest.mark.parametrize('route', [ + 'instance-outbox', + 'instance-inbox', + 'instance-actor', + 'well-known-webfinger', +]) +def test_instance_inbox_405_if_federation_disabled( + db, settings, api_client, route): + settings.FEDERATION_ENABLED = False + url = reverse('federation:{}'.format(route)) + response = api_client.get(url) + + assert response.status_code == 405 + + +def test_wellknown_webfinger_validates_resource( + db, api_client, settings, mocker): + clean = mocker.spy(webfinger, 'clean_resource') + settings.FEDERATION_ENABLED = True + url = reverse('federation:well-known-webfinger') + response = api_client.get(url, data={'resource': 'something'}) + + clean.assert_called_once_with('something') + assert url == '/.well-known/webfinger' + assert response.status_code == 400 + assert response.data['errors']['resource'] == ( + 'Missing webfinger resource type' + ) + + +def test_wellknown_webfinger_system( + db, api_client, settings, mocker): + settings.FEDERATION_ENABLED = True + settings.FEDERATION_HOSTNAME = 'test.federation' + url = reverse('federation:well-known-webfinger') + response = api_client.get( + url, data={'resource': 'acct:service@test.federation'}) + + assert response.status_code == 200 + assert response['Content-Type'] == 'application/jrd+json; charset=utf-8' + assert response.data == webfinger.serialize_system_acct() -- GitLab