summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--core/middleware.py13
-rw-r--r--core/signatures.py2
-rw-r--r--takahe/settings.py1
-rw-r--r--users/admin.py8
-rw-r--r--users/models/identity.py21
-rw-r--r--users/shortcuts.py9
-rw-r--r--users/tasks/follow.py5
-rw-r--r--users/tasks/inbox.py24
-rw-r--r--users/views/identity.py13
9 files changed, 71 insertions, 25 deletions
diff --git a/core/middleware.py b/core/middleware.py
new file mode 100644
index 0000000..8e95f06
--- /dev/null
+++ b/core/middleware.py
@@ -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
diff --git a/core/signatures.py b/core/signatures.py
index 6f4d9ef..805ae91 100644
--- a/core/signatures.py
+++ b/core/signatures.py
@@ -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",
diff --git a/takahe/settings.py b/takahe/settings.py
index 78a8403..fea5244 100644
--- a/takahe/settings.py
+++ b/takahe/settings.py
@@ -29,6 +29,7 @@ INSTALLED_APPS = [
]
MIDDLEWARE = [
+ "core.middleware.AlwaysSecureMiddleware",
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
diff --git a/users/admin.py b/users/admin.py
index bb07aa1..0b0cc80 100644
--- a/users/admin.py
+++ b/users/admin.py
@@ -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"]
diff --git a/users/models/identity.py b/users/models/identity.py
index 1f44e98..98262bc 100644
--- a/users/models/identity.py
+++ b/users/models/identity.py
@@ -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
diff --git a/users/shortcuts.py b/users/shortcuts.py
index 15b864d..65206a3 100644
--- a/users/shortcuts.py
+++ b/users/shortcuts.py
@@ -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
diff --git a/users/tasks/follow.py b/users/tasks/follow.py
index 3260124..872b35f 100644
--- a/users/tasks/follow.py
+++ b/users/tasks/follow.py
@@ -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)
diff --git a/users/tasks/inbox.py b/users/tasks/inbox.py
index ab80648..27c602d 100644
--- a/users/tasks/inbox.py
+++ b/users/tasks/inbox.py
@@ -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()
diff --git a/users/views/identity.py b/users/views/identity.py
index 7cba43e..98fcdd6 100644
--- a/users/views/identity.py
+++ b/users/views/identity.py
@@ -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,
},
],
}