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(
|
headers["Signature"] = self.compile_signature(
|
||||||
{
|
{
|
||||||
"keyid": identity.urls.key.full(), # type:ignore
|
"keyid": identity.key_id,
|
||||||
"headers": list(headers.keys()),
|
"headers": list(headers.keys()),
|
||||||
"signature": identity.sign(signed_string),
|
"signature": identity.sign(signed_string),
|
||||||
"algorithm": "rsa-sha256",
|
"algorithm": "rsa-sha256",
|
||||||
|
|
|
@ -29,6 +29,7 @@ INSTALLED_APPS = [
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
|
"core.middleware.AlwaysSecureMiddleware",
|
||||||
"django.middleware.security.SecurityMiddleware",
|
"django.middleware.security.SecurityMiddleware",
|
||||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||||
"django.middleware.common.CommonMiddleware",
|
"django.middleware.common.CommonMiddleware",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
from django.contrib import admin
|
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)
|
@admin.register(Domain)
|
||||||
|
@ -20,5 +20,9 @@ class UserEventAdmin(admin.ModelAdmin):
|
||||||
|
|
||||||
@admin.register(Identity)
|
@admin.register(Identity)
|
||||||
class IdentityAdmin(admin.ModelAdmin):
|
class IdentityAdmin(admin.ModelAdmin):
|
||||||
|
|
||||||
list_display = ["id", "handle", "actor_uri", "name", "local"]
|
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 = "/@{self.username}@{self.domain_id}/"
|
||||||
view_short = "/@{self.username}/"
|
view_short = "/@{self.username}/"
|
||||||
action = "{view}action/"
|
action = "{view}action/"
|
||||||
actor = "{view}actor/"
|
|
||||||
activate = "{view}activate/"
|
activate = "{view}activate/"
|
||||||
key = "{actor}#main-key"
|
|
||||||
inbox = "{actor}inbox/"
|
|
||||||
outbox = "{actor}outbox/"
|
|
||||||
|
|
||||||
def get_scheme(self, url):
|
def get_scheme(self, url):
|
||||||
return "https"
|
return "https"
|
||||||
|
@ -102,12 +98,9 @@ class Identity(models.Model):
|
||||||
### Alternate constructors/fetchers ###
|
### Alternate constructors/fetchers ###
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def by_handle(cls, handle, fetch=False, local=False):
|
def by_username_and_domain(cls, username, domain, fetch=False, local=False):
|
||||||
if handle.startswith("@"):
|
if username.startswith("@"):
|
||||||
raise ValueError("Handle must not start with @")
|
raise ValueError("Username must not start with @")
|
||||||
if "@" not in handle:
|
|
||||||
raise ValueError("Handle must contain domain")
|
|
||||||
username, domain = handle.split("@")
|
|
||||||
try:
|
try:
|
||||||
if local:
|
if local:
|
||||||
return cls.objects.get(username=username, domain_id=domain, local=True)
|
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)
|
return cls.objects.get(username=username, domain_id=domain)
|
||||||
except cls.DoesNotExist:
|
except cls.DoesNotExist:
|
||||||
if fetch and not local:
|
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("@")
|
username, domain = handle.split("@")
|
||||||
domain = Domain.get_remote_domain(domain)
|
domain = Domain.get_remote_domain(domain)
|
||||||
return cls.objects.create(
|
return cls.objects.create(
|
||||||
|
@ -168,6 +163,10 @@ class Identity(models.Model):
|
||||||
# TODO: Setting
|
# TODO: Setting
|
||||||
return self.data_age > 60 * 24 * 24
|
return self.data_age > 60 * 24 * 24
|
||||||
|
|
||||||
|
@property
|
||||||
|
def key_id(self):
|
||||||
|
return self.actor_uri + "#main-key"
|
||||||
|
|
||||||
### Actor/Webfinger fetching ###
|
### Actor/Webfinger fetching ###
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
|
@ -18,7 +18,14 @@ def by_handle_or_404(request, handle, local=True, fetch=False):
|
||||||
domain = domain_instance.domain
|
domain = domain_instance.domain
|
||||||
else:
|
else:
|
||||||
username, domain = handle.split("@", 1)
|
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:
|
if identity is None:
|
||||||
raise Http404(f"No identity for handle {handle}")
|
raise Http404(f"No identity for handle {handle}")
|
||||||
return identity
|
return identity
|
||||||
|
|
|
@ -24,5 +24,6 @@ async def handle_follow_request(task_handler):
|
||||||
response = await HttpSignature.signed_request(
|
response = await HttpSignature.signed_request(
|
||||||
follow.target.inbox_uri, request, follow.source
|
follow.target.inbox_uri, request, follow.source
|
||||||
)
|
)
|
||||||
print(response)
|
if response.status_code >= 400:
|
||||||
print(response.content)
|
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
|
from users.models import Follow, Identity
|
||||||
|
|
||||||
|
|
||||||
|
@ -5,14 +7,20 @@ async def handle_inbox_item(task_handler):
|
||||||
type = task_handler.payload["type"].lower()
|
type = task_handler.payload["type"].lower()
|
||||||
if type == "follow":
|
if type == "follow":
|
||||||
await inbox_follow(task_handler.payload)
|
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":
|
elif type == "undo":
|
||||||
inner_type = task_handler.payload["object"]["type"].lower()
|
inner_type = task_handler.payload["object"]["type"].lower()
|
||||||
if inner_type == "follow":
|
if inner_type == "follow":
|
||||||
await inbox_unfollow(task_handler.payload["object"])
|
await inbox_unfollow(task_handler.payload["object"])
|
||||||
else:
|
else:
|
||||||
raise ValueError("Cannot undo activity of type {inner_type}")
|
raise ValueError(f"Cannot handle activity of type undo.{inner_type}")
|
||||||
else:
|
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):
|
async def inbox_follow(payload):
|
||||||
|
@ -34,3 +42,15 @@ async def inbox_follow(payload):
|
||||||
|
|
||||||
async def inbox_unfollow(payload):
|
async def inbox_unfollow(payload):
|
||||||
pass
|
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):
|
def form_valid(self, form):
|
||||||
username = form.cleaned_data["username"]
|
username = form.cleaned_data["username"]
|
||||||
domain = form.cleaned_data["domain"]
|
domain = form.cleaned_data["domain"]
|
||||||
|
domain_instance = Domain.get_local_domain(domain)
|
||||||
new_identity = Identity.objects.create(
|
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,
|
username=username,
|
||||||
domain_id=domain,
|
domain_id=domain,
|
||||||
name=form.cleaned_data["name"],
|
name=form.cleaned_data["name"],
|
||||||
|
@ -154,13 +155,13 @@ class Actor(View):
|
||||||
"https://www.w3.org/ns/activitystreams",
|
"https://www.w3.org/ns/activitystreams",
|
||||||
"https://w3id.org/security/v1",
|
"https://w3id.org/security/v1",
|
||||||
],
|
],
|
||||||
"id": identity.urls.actor.full(),
|
"id": identity.actor_uri,
|
||||||
"type": "Person",
|
"type": "Person",
|
||||||
"inbox": identity.urls.inbox.full(),
|
"inbox": identity.actor_uri + "inbox/",
|
||||||
"preferredUsername": identity.username,
|
"preferredUsername": identity.username,
|
||||||
"publicKey": {
|
"publicKey": {
|
||||||
"id": identity.urls.key.full(),
|
"id": identity.key_id,
|
||||||
"owner": identity.urls.actor.full(),
|
"owner": identity.actor_uri,
|
||||||
"publicKeyPem": identity.public_key,
|
"publicKeyPem": identity.public_key,
|
||||||
},
|
},
|
||||||
"published": identity.created.strftime("%Y-%m-%dT%H:%M:%SZ"),
|
"published": identity.created.strftime("%Y-%m-%dT%H:%M:%SZ"),
|
||||||
|
@ -249,7 +250,7 @@ class Webfinger(View):
|
||||||
{
|
{
|
||||||
"rel": "self",
|
"rel": "self",
|
||||||
"type": "application/activity+json",
|
"type": "application/activity+json",
|
||||||
"href": identity.urls.actor.full(),
|
"href": identity.actor_uri,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue