From 9def8c31940e0a52acb019fa79ba3c2efcd8c55a Mon Sep 17 00:00:00 2001 From: Jean-Baptiste <bleme@pm.me> Date: Wed, 21 Aug 2019 11:15:02 +0200 Subject: [PATCH] update: extensible webfinger --- djangoldp/endpoints/__init__.py | 0 djangoldp/endpoints/webfinger.py | 107 +++++++++++++++++++++++++++++++ djangoldp/urls.py | 3 +- djangoldp/views.py | 24 +++++++ 4 files changed, 133 insertions(+), 1 deletion(-) create mode 100644 djangoldp/endpoints/__init__.py create mode 100644 djangoldp/endpoints/webfinger.py diff --git a/djangoldp/endpoints/__init__.py b/djangoldp/endpoints/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/djangoldp/endpoints/webfinger.py b/djangoldp/endpoints/webfinger.py new file mode 100644 index 00000000..7cbf1cc4 --- /dev/null +++ b/djangoldp/endpoints/webfinger.py @@ -0,0 +1,107 @@ +import re +from importlib import import_module + +from django.conf import settings + + +class WebFinger(object): + def response(self, response_dict, rel, acct): + return response_dict + + +for package in settings.DJANGOLDP_PACKAGES: + try: + import_module('{}.endpoints.webfinger'.format(package)) + except ModuleNotFoundError: + pass + +model_classes = {cls.__name__: cls for cls in WebFinger.__subclasses__()} + +ACCT_RE = re.compile( + r'(?:acct:)?(?P<userinfo>[\w.!#$%&\'*+-/=?^_`{|}~]+)@(?P<host>[\w.:-]+)') + + +class Acct(object): + def __init__(self, acct): + m = ACCT_RE.match(acct) + if not m: + raise ValueError('invalid acct format') + (userinfo, host) = m.groups() + self.userinfo = userinfo + self.host = host + + +class WebFingerEndpoint(object): + """ + WebFinger endpoint + See https://tools.ietf.org/html/rfc7033 + """ + + def __init__(self, request): + self.request = request + self.params = {} + self.acct = None + + self._extract_params() + + def _extract_params(self): + # Because in this endpoint we handle both GET + # and POST request. + query_dict = (self.request.POST if self.request.method == 'POST' + else self.request.GET) + + self.params['resource'] = query_dict.get('resource', None) + self.params['rel'] = query_dict.get('rel', '') + + def validate_params(self): + """ + A resource must be set. + """ + + if self.params['resource'] is None: + raise WebFingerError('invalid_request') + + try: + self.acct = Acct(self.params['resource']) + except ValueError: + raise WebFingerError('invalid_acct_format') + + def response(self): + """ + This endpoint only reply to rel="http://openid.net/specs/connect/1.0/issuer" + :return: a dict representing the Json response + """ + + dict = { + 'subject': self.params['resource'], + 'links': [] + } + + for class_name in model_classes: + webfinger = class_name() + dict = webfinger.response(dict, self.params['rel'], self.acct) + + return dict + + +class WebFingerError(Exception): + _errors = { + 'invalid_request': "The request provider parameter must contains an url or an email", + 'invalid_acct_format': "Invalid acct format" + } + + def __init__(self, error=None, dict=None): + if dict is None: + self.error = error + self.description = self._errors.get(error) + else: + self.error = dict['error'] + self.description = dict['error_description'] + + def create_dict(self): + dic = { + 'error': self.error, + 'error_description': self.description, + } + + return dic diff --git a/djangoldp/urls.py b/djangoldp/urls.py index 39b9e351..4fd8d7f4 100644 --- a/djangoldp/urls.py +++ b/djangoldp/urls.py @@ -5,7 +5,7 @@ from django.conf.urls import url, include from djangoldp.models import LDPSource, Model from djangoldp.permissions import LDPPermissions -from djangoldp.views import LDPSourceViewSet +from djangoldp.views import LDPSourceViewSet, WebFingerView def __clean_path(path): @@ -19,6 +19,7 @@ def __clean_path(path): urlpatterns = [ url(r'^sources/(?P<federation>\w+)/', LDPSourceViewSet.urls(model=LDPSource, fields=['federation', 'container'], permission_classes=[LDPPermissions], )), + url(r'^\.well-known/webfinger/?$', WebFingerView.as_view()), ] for package in settings.DJANGOLDP_PACKAGES: diff --git a/djangoldp/views.py b/djangoldp/views.py index 48210094..748751fc 100644 --- a/djangoldp/views.py +++ b/djangoldp/views.py @@ -4,8 +4,10 @@ from django.conf.urls import url, include from django.contrib.auth import get_user_model from django.core.exceptions import FieldDoesNotExist from django.core.urlresolvers import get_resolver +from django.http import JsonResponse from django.shortcuts import get_object_or_404 from django.utils.decorators import classonlymethod +from django.views import View from pyld import jsonld from rest_framework import status from rest_framework.authentication import SessionAuthentication @@ -14,6 +16,7 @@ from rest_framework.renderers import JSONRenderer from rest_framework.response import Response from rest_framework.viewsets import ModelViewSet +from djangoldp.endpoints.webfinger import WebFingerEndpoint, WebFingerError from djangoldp.models import LDPSource, Model from djangoldp.permissions import LDPPermissions @@ -278,3 +281,24 @@ class LDPSourceViewSet(LDPViewSet): def get_queryset(self, *args, **kwargs): return super().get_queryset(*args, **kwargs).filter(federation=self.kwargs['federation']) + + +class WebFingerView(View): + endpoint_class = WebFingerEndpoint + + def get(self, request, *args, **kwargs): + return self.on_request(request) + + def on_request(self, request): + endpoint = self.endpoint_class(request) + try: + endpoint.validate_params() + + return JsonResponse(endpoint.response()) + + except WebFingerError as error: + return JsonResponse(error.create_dict(), status=400) + + def post(self, request, *args, **kwargs): + return self.on_request(request) + -- GitLab