parent
fb6c409a9a
commit
c391e7bc41
|
@ -0,0 +1,13 @@
|
|||
class AlwaysSecureMiddleware:
|
||||
"""
|
||||
Locks the request object as always being secure, for when it's behind
|
||||
a HTTPS reverse proxy.
|
||||
"""
|
||||
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request):
|
||||
request.__class__.scheme = "https"
|
||||
response = self.get_response(request)
|
||||
return response
|
|
@ -96,7 +96,7 @@ class HttpSignature:
|
|||
)
|
||||
headers["Signature"] = self.compile_signature(
|
||||
{
|
||||
"keyid": identity.urls.key.full(), # type:ignore
|
||||
"keyid": identity.key_id,
|
||||
"headers": list(headers.keys()),
|
||||
"signature": identity.sign(signed_string),
|
||||
"algorithm": "rsa-sha256",
|
||||
|
|
|
@ -29,6 +29,7 @@ INSTALLED_APPS = [
|
|||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
"core.middleware.AlwaysSecureMiddleware",
|
||||
"django.middleware.security.SecurityMiddleware",
|
||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||
"django.middleware.common.CommonMiddleware",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
from django.contrib import admin
|
||||
|
||||
from users.models import Domain, Identity, User, UserEvent
|
||||
from users.models import Domain, Follow, Identity, User, UserEvent
|
||||
|
||||
|
||||
@admin.register(Domain)
|
||||
|
@ -20,5 +20,9 @@ class UserEventAdmin(admin.ModelAdmin):
|
|||
|
||||
@admin.register(Identity)
|
||||
class IdentityAdmin(admin.ModelAdmin):
|
||||
|
||||
list_display = ["id", "handle", "actor_uri", "name", "local"]
|
||||
|
||||
|
||||
@admin.register(Follow)
|
||||
class FollowAdmin(admin.ModelAdmin):
|
||||
list_display = ["id", "source", "target", "requested", "accepted"]
|
||||
|
|
|
@ -82,11 +82,7 @@ class Identity(models.Model):
|
|||
view = "/@{self.username}@{self.domain_id}/"
|
||||
view_short = "/@{self.username}/"
|
||||
action = "{view}action/"
|
||||
actor = "{view}actor/"
|
||||
activate = "{view}activate/"
|
||||
key = "{actor}#main-key"
|
||||
inbox = "{actor}inbox/"
|
||||
outbox = "{actor}outbox/"
|
||||
|
||||
def get_scheme(self, url):
|
||||
return "https"
|
||||
|
@ -102,12 +98,9 @@ class Identity(models.Model):
|
|||
### Alternate constructors/fetchers ###
|
||||
|
||||
@classmethod
|
||||
def by_handle(cls, handle, fetch=False, local=False):
|
||||
if handle.startswith("@"):
|
||||
raise ValueError("Handle must not start with @")
|
||||
if "@" not in handle:
|
||||
raise ValueError("Handle must contain domain")
|
||||
username, domain = handle.split("@")
|
||||
def by_username_and_domain(cls, username, domain, fetch=False, local=False):
|
||||
if username.startswith("@"):
|
||||
raise ValueError("Username must not start with @")
|
||||
try:
|
||||
if local:
|
||||
return cls.objects.get(username=username, domain_id=domain, local=True)
|
||||
|
@ -115,7 +108,9 @@ class Identity(models.Model):
|
|||
return cls.objects.get(username=username, domain_id=domain)
|
||||
except cls.DoesNotExist:
|
||||
if fetch and not local:
|
||||
actor_uri, handle = async_to_sync(cls.fetch_webfinger)(handle)
|
||||
actor_uri, handle = async_to_sync(cls.fetch_webfinger)(
|
||||
f"{username}@{domain}"
|
||||
)
|
||||
username, domain = handle.split("@")
|
||||
domain = Domain.get_remote_domain(domain)
|
||||
return cls.objects.create(
|
||||
|
@ -168,6 +163,10 @@ class Identity(models.Model):
|
|||
# TODO: Setting
|
||||
return self.data_age > 60 * 24 * 24
|
||||
|
||||
@property
|
||||
def key_id(self):
|
||||
return self.actor_uri + "#main-key"
|
||||
|
||||
### Actor/Webfinger fetching ###
|
||||
|
||||
@classmethod
|
||||
|
|
|
@ -18,7 +18,14 @@ def by_handle_or_404(request, handle, local=True, fetch=False):
|
|||
domain = domain_instance.domain
|
||||
else:
|
||||
username, domain = handle.split("@", 1)
|
||||
identity = Identity.by_handle(handle, local=local, fetch=fetch)
|
||||
# Resolve the domain to the display domain
|
||||
domain = Domain.get_local_domain(request.META["HTTP_HOST"]).domain
|
||||
identity = Identity.by_username_and_domain(
|
||||
username,
|
||||
domain,
|
||||
local=local,
|
||||
fetch=fetch,
|
||||
)
|
||||
if identity is None:
|
||||
raise Http404(f"No identity for handle {handle}")
|
||||
return identity
|
||||
|
|
|
@ -24,5 +24,6 @@ async def handle_follow_request(task_handler):
|
|||
response = await HttpSignature.signed_request(
|
||||
follow.target.inbox_uri, request, follow.source
|
||||
)
|
||||
print(response)
|
||||
print(response.content)
|
||||
if response.status_code >= 400:
|
||||
raise ValueError(f"Request error: {response.status_code} {response.content}")
|
||||
await Follow.objects.filter(pk=follow.pk).aupdate(requested=True)
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
from asgiref.sync import sync_to_async
|
||||
|
||||
from users.models import Follow, Identity
|
||||
|
||||
|
||||
|
@ -5,14 +7,20 @@ async def handle_inbox_item(task_handler):
|
|||
type = task_handler.payload["type"].lower()
|
||||
if type == "follow":
|
||||
await inbox_follow(task_handler.payload)
|
||||
elif type == "accept":
|
||||
inner_type = task_handler.payload["object"]["type"].lower()
|
||||
if inner_type == "follow":
|
||||
await sync_to_async(accept_follow)(task_handler.payload["object"])
|
||||
else:
|
||||
raise ValueError(f"Cannot handle activity of type accept.{inner_type}")
|
||||
elif type == "undo":
|
||||
inner_type = task_handler.payload["object"]["type"].lower()
|
||||
if inner_type == "follow":
|
||||
await inbox_unfollow(task_handler.payload["object"])
|
||||
else:
|
||||
raise ValueError("Cannot undo activity of type {inner_type}")
|
||||
raise ValueError(f"Cannot handle activity of type undo.{inner_type}")
|
||||
else:
|
||||
raise ValueError("Cannot handle activity of type {inner_type}")
|
||||
raise ValueError(f"Cannot handle activity of type {inner_type}")
|
||||
|
||||
|
||||
async def inbox_follow(payload):
|
||||
|
@ -34,3 +42,15 @@ async def inbox_follow(payload):
|
|||
|
||||
async def inbox_unfollow(payload):
|
||||
pass
|
||||
|
||||
|
||||
def accept_follow(payload):
|
||||
"""
|
||||
Another server has acknowledged our follow request
|
||||
"""
|
||||
source = Identity.by_actor_uri_with_create(payload["actor"])
|
||||
target = Identity.by_actor_uri(payload["object"])
|
||||
follow = Follow.maybe_get(source, target)
|
||||
if follow:
|
||||
follow.accepted = True
|
||||
follow.save()
|
||||
|
|
|
@ -130,8 +130,9 @@ class CreateIdentity(FormView):
|
|||
def form_valid(self, form):
|
||||
username = form.cleaned_data["username"]
|
||||
domain = form.cleaned_data["domain"]
|
||||
domain_instance = Domain.get_local_domain(domain)
|
||||
new_identity = Identity.objects.create(
|
||||
actor_uri=f"https://{domain}/@{username}/actor/",
|
||||
actor_uri=f"https://{domain_instance.uri_domain}/@{username}@{domain}/actor/",
|
||||
username=username,
|
||||
domain_id=domain,
|
||||
name=form.cleaned_data["name"],
|
||||
|
@ -154,13 +155,13 @@ class Actor(View):
|
|||
"https://www.w3.org/ns/activitystreams",
|
||||
"https://w3id.org/security/v1",
|
||||
],
|
||||
"id": identity.urls.actor.full(),
|
||||
"id": identity.actor_uri,
|
||||
"type": "Person",
|
||||
"inbox": identity.urls.inbox.full(),
|
||||
"inbox": identity.actor_uri + "inbox/",
|
||||
"preferredUsername": identity.username,
|
||||
"publicKey": {
|
||||
"id": identity.urls.key.full(),
|
||||
"owner": identity.urls.actor.full(),
|
||||
"id": identity.key_id,
|
||||
"owner": identity.actor_uri,
|
||||
"publicKeyPem": identity.public_key,
|
||||
},
|
||||
"published": identity.created.strftime("%Y-%m-%dT%H:%M:%SZ"),
|
||||
|
@ -249,7 +250,7 @@ class Webfinger(View):
|
|||
{
|
||||
"rel": "self",
|
||||
"type": "application/activity+json",
|
||||
"href": identity.urls.actor.full(),
|
||||
"href": identity.actor_uri,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue