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