From 878f56b411279cd9865a7ec05f1d14c9f70f6187 Mon Sep 17 00:00:00 2001 From: Andrew Godwin Date: Sat, 12 Nov 2022 21:14:21 -0700 Subject: Post URIs and host-meta --- users/admin.py | 2 +- users/models/follow.py | 6 +- users/models/inbox_message.py | 4 ++ users/views/activitypub.py | 148 ++++++++++++++++++++++++++++++++++++++++++ users/views/identity.py | 129 +----------------------------------- 5 files changed, 158 insertions(+), 131 deletions(-) create mode 100644 users/views/activitypub.py (limited to 'users') diff --git a/users/admin.py b/users/admin.py index 61a2bc8..e52e41c 100644 --- a/users/admin.py +++ b/users/admin.py @@ -38,7 +38,7 @@ class FollowAdmin(admin.ModelAdmin): @admin.register(InboxMessage) class InboxMessageAdmin(admin.ModelAdmin): - list_display = ["id", "state", "state_attempted", "message_type"] + list_display = ["id", "state", "state_attempted", "message_type", "message_actor"] actions = ["reset_state"] @admin.action(description="Reset State") diff --git a/users/models/follow.py b/users/models/follow.py index 238081e..0236d19 100644 --- a/users/models/follow.py +++ b/users/models/follow.py @@ -37,7 +37,7 @@ class FollowStates(StateGraph): await HttpSignature.signed_request( uri=follow.target.inbox_uri, body=canonicalise(follow.to_ap()), - private_key=follow.source.public_key, + private_key=follow.source.private_key, key_id=follow.source.public_key_id, ) return cls.local_requested @@ -57,7 +57,7 @@ class FollowStates(StateGraph): await HttpSignature.signed_request( uri=follow.source.inbox_uri, body=canonicalise(follow.to_accept_ap()), - private_key=follow.target.public_key, + private_key=follow.target.private_key, key_id=follow.target.public_key_id, ) return cls.accepted @@ -71,7 +71,7 @@ class FollowStates(StateGraph): await HttpSignature.signed_request( uri=follow.target.inbox_uri, body=canonicalise(follow.to_undo_ap()), - private_key=follow.source.public_key, + private_key=follow.source.private_key, key_id=follow.source.public_key_id, ) return cls.undone_remotely diff --git a/users/models/inbox_message.py b/users/models/inbox_message.py index ea55b17..43424c9 100644 --- a/users/models/inbox_message.py +++ b/users/models/inbox_message.py @@ -66,3 +66,7 @@ class InboxMessage(StatorModel): @property def message_object_type(self): return self.message["object"]["type"].lower() + + @property + def message_actor(self): + return self.message.get("actor") diff --git a/users/views/activitypub.py b/users/views/activitypub.py new file mode 100644 index 0000000..54f04bc --- /dev/null +++ b/users/views/activitypub.py @@ -0,0 +1,148 @@ +import json + +from asgiref.sync import async_to_sync +from django.http import Http404, HttpResponse, HttpResponseBadRequest, JsonResponse +from django.utils.decorators import method_decorator +from django.views.decorators.csrf import csrf_exempt +from django.views.generic import View + +from core.ld import canonicalise +from core.signatures import ( + HttpSignature, + LDSignature, + VerificationError, + VerificationFormatError, +) +from users.models import Identity, InboxMessage +from users.shortcuts import by_handle_or_404 + + +class HttpResponseUnauthorized(HttpResponse): + status_code = 401 + + +class HostMeta(View): + """ + Returns a canned host-meta response + """ + + def get(self, request): + return HttpResponse( + """ + + + """ + % request.META["HTTP_HOST"], + content_type="application/xml", + ) + + +class Webfinger(View): + """ + Services webfinger requests + """ + + def get(self, request): + resource = request.GET.get("resource") + if not resource.startswith("acct:"): + raise Http404("Not an account resource") + handle = resource[5:].replace("testfedi", "feditest") + identity = by_handle_or_404(request, handle) + return JsonResponse( + { + "subject": f"acct:{identity.handle}", + "aliases": [ + identity.urls.view_short.full(), + ], + "links": [ + { + "rel": "http://webfinger.net/rel/profile-page", + "type": "text/html", + "href": identity.urls.view_short.full(), + }, + { + "rel": "self", + "type": "application/activity+json", + "href": identity.actor_uri, + }, + ], + } + ) + + +class Actor(View): + """ + Returns the AP Actor object + """ + + def get(self, request, handle): + identity = by_handle_or_404(self.request, handle) + response = { + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + ], + "id": identity.actor_uri, + "type": "Person", + "inbox": identity.actor_uri + "inbox/", + "preferredUsername": identity.username, + "publicKey": { + "id": identity.public_key_id, + "owner": identity.actor_uri, + "publicKeyPem": identity.public_key, + }, + "published": identity.created.strftime("%Y-%m-%dT%H:%M:%SZ"), + "url": identity.urls.view_short.full(), + } + if identity.name: + response["name"] = identity.name + if identity.summary: + response["summary"] = identity.summary + return JsonResponse(canonicalise(response, include_security=True)) + + +@method_decorator(csrf_exempt, name="dispatch") +class Inbox(View): + """ + AP Inbox endpoint + """ + + def post(self, request, handle): + # Load the LD + document = canonicalise(json.loads(request.body), include_security=True) + # Find the Identity by the actor on the incoming item + # This ensures that the signature used for the headers matches the actor + # described in the payload. + identity = Identity.by_actor_uri(document["actor"], create=True) + if not identity.public_key: + # See if we can fetch it right now + async_to_sync(identity.fetch_actor)() + if not identity.public_key: + print("Cannot get actor") + return HttpResponseBadRequest("Cannot retrieve actor") + # If there's a "signature" payload, verify against that + if "signature" in document: + try: + LDSignature.verify_signature(document, identity.public_key) + except VerificationFormatError as e: + print("Bad LD signature format:", e.args[0]) + return HttpResponseBadRequest(e.args[0]) + except VerificationError: + print("Bad LD signature") + return HttpResponseUnauthorized("Bad signature") + # Otherwise, verify against the header (assuming it's the same actor) + else: + try: + HttpSignature.verify_request( + request, + identity.public_key, + ) + except VerificationFormatError as e: + print("Bad HTTP signature format:", e.args[0]) + return HttpResponseBadRequest(e.args[0]) + except VerificationError: + print("Bad HTTP signature") + return HttpResponseUnauthorized("Bad signature") + # Hand off the item to be processed by the queue + InboxMessage.objects.create(message=document) + return HttpResponse(status=202) diff --git a/users/views/identity.py b/users/views/identity.py index d4e1155..5d11d63 100644 --- a/users/views/identity.py +++ b/users/views/identity.py @@ -1,33 +1,19 @@ -import json import string -from asgiref.sync import async_to_sync from django import forms from django.conf import settings from django.contrib.auth.decorators import login_required -from django.http import Http404, HttpResponse, HttpResponseBadRequest, JsonResponse +from django.http import Http404 from django.shortcuts import redirect from django.utils.decorators import method_decorator -from django.views.decorators.csrf import csrf_exempt from django.views.generic import FormView, TemplateView, View from core.forms import FormHelper -from core.ld import canonicalise -from core.signatures import ( - HttpSignature, - LDSignature, - VerificationError, - VerificationFormatError, -) from users.decorators import identity_required -from users.models import Domain, Follow, Identity, IdentityStates, InboxMessage +from users.models import Domain, Follow, Identity, IdentityStates from users.shortcuts import by_handle_or_404 -class HttpResponseUnauthorized(HttpResponse): - status_code = 401 - - class ViewIdentity(TemplateView): template_name = "identity/view.html" @@ -151,114 +137,3 @@ class CreateIdentity(FormView): new_identity.users.add(self.request.user) new_identity.generate_keypair() return redirect(new_identity.urls.view) - - -class Actor(View): - """ - Returns the AP Actor object - """ - - def get(self, request, handle): - identity = by_handle_or_404(self.request, handle) - response = { - "@context": [ - "https://www.w3.org/ns/activitystreams", - "https://w3id.org/security/v1", - ], - "id": identity.actor_uri, - "type": "Person", - "inbox": identity.actor_uri + "inbox/", - "preferredUsername": identity.username, - "publicKey": { - "id": identity.public_key_id, - "owner": identity.actor_uri, - "publicKeyPem": identity.public_key, - }, - "published": identity.created.strftime("%Y-%m-%dT%H:%M:%SZ"), - "url": identity.urls.view_short.full(), - } - if identity.name: - response["name"] = identity.name - if identity.summary: - response["summary"] = identity.summary - return JsonResponse(canonicalise(response, include_security=True)) - - -@method_decorator(csrf_exempt, name="dispatch") -class Inbox(View): - """ - AP Inbox endpoint - """ - - def post(self, request, handle): - # Load the LD - document = canonicalise(json.loads(request.body), include_security=True) - # Find the Identity by the actor on the incoming item - # This ensures that the signature used for the headers matches the actor - # described in the payload. - identity = Identity.by_actor_uri(document["actor"], create=True) - if not identity.public_key: - # See if we can fetch it right now - async_to_sync(identity.fetch_actor)() - if not identity.public_key: - print("Cannot get actor") - return HttpResponseBadRequest("Cannot retrieve actor") - # If there's a "signature" payload, verify against that - if "signature" in document: - try: - LDSignature.verify_signature(document, identity.public_key) - except VerificationFormatError as e: - print("Bad LD signature format:", e.args[0]) - return HttpResponseBadRequest(e.args[0]) - except VerificationError: - print("Bad LD signature") - return HttpResponseUnauthorized("Bad signature") - # Otherwise, verify against the header (assuming it's the same actor) - else: - try: - HttpSignature.verify_request( - request, - identity.public_key, - ) - except VerificationFormatError as e: - print("Bad HTTP signature format:", e.args[0]) - return HttpResponseBadRequest(e.args[0]) - except VerificationError: - print("Bad HTTP signature") - return HttpResponseUnauthorized("Bad signature") - # Hand off the item to be processed by the queue - InboxMessage.objects.create(message=document) - return HttpResponse(status=202) - - -class Webfinger(View): - """ - Services webfinger requests - """ - - def get(self, request): - resource = request.GET.get("resource") - if not resource.startswith("acct:"): - raise Http404("Not an account resource") - handle = resource[5:].replace("testfedi", "feditest") - identity = by_handle_or_404(request, handle) - return JsonResponse( - { - "subject": f"acct:{identity.handle}", - "aliases": [ - identity.urls.view_short.full(), - ], - "links": [ - { - "rel": "http://webfinger.net/rel/profile-page", - "type": "text/html", - "href": identity.urls.view_short.full(), - }, - { - "rel": "self", - "type": "application/activity+json", - "href": identity.actor_uri, - }, - ], - } - ) -- cgit v1.2.3