From fb6c409a9af5b8a686e977ee2251c359071e0ec3 Mon Sep 17 00:00:00 2001 From: Andrew Godwin Date: Sun, 6 Nov 2022 21:30:07 -0700 Subject: Rework task system and fetching. I can taste how close follow is to working. --- core/ld.py | 59 ++++++++++++++++++++++++++++++++++++++++++----- core/signatures.py | 67 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 117 insertions(+), 9 deletions(-) (limited to 'core') diff --git a/core/ld.py b/core/ld.py index 28ff65a..2211ba9 100644 --- a/core/ld.py +++ b/core/ld.py @@ -227,6 +227,49 @@ schemas = { } }, }, + "*/schemas/litepub-0.1.jsonld": { + "contentType": "application/ld+json", + "documentUrl": "http://w3id.org/security/v1", + "contextUrl": None, + "document": { + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + { + "Emoji": "toot:Emoji", + "Hashtag": "as:Hashtag", + "PropertyValue": "schema:PropertyValue", + "atomUri": "ostatus:atomUri", + "conversation": {"@id": "ostatus:conversation", "@type": "@id"}, + "discoverable": "toot:discoverable", + "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", + "capabilities": "litepub:capabilities", + "ostatus": "http://ostatus.org#", + "schema": "http://schema.org#", + "toot": "http://joinmastodon.org/ns#", + "misskey": "https://misskey-hub.net/ns#", + "fedibird": "http://fedibird.com/ns#", + "value": "schema:value", + "sensitive": "as:sensitive", + "litepub": "http://litepub.social/ns#", + "invisible": "litepub:invisible", + "directMessage": "litepub:directMessage", + "listMessage": {"@id": "litepub:listMessage", "@type": "@id"}, + "quoteUrl": "as:quoteUrl", + "quoteUri": "fedibird:quoteUri", + "oauthRegistrationEndpoint": { + "@id": "litepub:oauthRegistrationEndpoint", + "@type": "@id", + }, + "EmojiReact": "litepub:EmojiReact", + "ChatMessage": "litepub:ChatMessage", + "alsoKnownAs": {"@id": "as:alsoKnownAs", "@type": "@id"}, + "vcard": "http://www.w3.org/2006/vcard/ns#", + "formerRepresentations": "litepub:formerRepresentations", + }, + ] + }, + }, } @@ -244,12 +287,16 @@ def builtin_document_loader(url: str, options={}): try: return schemas[key] except KeyError: - raise JsonLdError( - f"No schema built-in for {key!r}", - "jsonld.LoadDocumentError", - code="loading document failed", - cause="KeyError", - ) + try: + key = "*" + pieces.path.rstrip("/") + return schemas[key] + except KeyError: + raise JsonLdError( + f"No schema built-in for {key!r}", + "jsonld.LoadDocumentError", + code="loading document failed", + cause="KeyError", + ) def canonicalise(json_data, include_security=False): diff --git a/core/signatures.py b/core/signatures.py index a5e4fed..6f4d9ef 100644 --- a/core/signatures.py +++ b/core/signatures.py @@ -1,8 +1,14 @@ import base64 -from typing import List, TypedDict +import json +from typing import Dict, List, Literal, TypedDict +from urllib.parse import urlparse +import httpx from cryptography.hazmat.primitives import hashes from django.http import HttpRequest +from django.utils.http import http_date + +from users.models import Identity class HttpSignature: @@ -25,7 +31,8 @@ class HttpSignature: @classmethod def headers_from_request(cls, request: HttpRequest, header_names: List[str]) -> str: """ - Creates the to-be-signed header payload from a Django request""" + Creates the to-be-signed header payload from a Django request + """ headers = {} for header_name in header_names: if header_name == "(request-target)": @@ -38,7 +45,7 @@ class HttpSignature: return "\n".join(f"{name.lower()}: {value}" for name, value in headers.items()) @classmethod - def parse_signature(cls, signature) -> "SignatureDetails": + def parse_signature(cls, signature: str) -> "SignatureDetails": bits = {} for item in signature.split(","): name, value = item.split("=", 1) @@ -52,6 +59,60 @@ class HttpSignature: } return signature_details + @classmethod + def compile_signature(cls, details: "SignatureDetails") -> str: + value = f'keyId="{details["keyid"]}",headers="' + value += " ".join(h.lower() for h in details["headers"]) + value += '",signature="' + value += base64.b64encode(details["signature"]).decode("ascii") + value += f'",algorithm="{details["algorithm"]}"' + return value + + @classmethod + async def signed_request( + self, + uri: str, + body: Dict, + identity: Identity, + content_type: str = "application/json", + method: Literal["post"] = "post", + ): + """ + Performs an async request to the given path, with a document, signed + as an identity. + """ + uri_parts = urlparse(uri) + date_string = http_date() + body_bytes = json.dumps(body).encode("utf8") + headers = { + "(request-target)": f"{method} {uri_parts.path}", + "Host": uri_parts.hostname, + "Date": date_string, + "Digest": self.calculate_digest(body_bytes), + "Content-Type": content_type, + } + signed_string = "\n".join( + f"{name.lower()}: {value}" for name, value in headers.items() + ) + headers["Signature"] = self.compile_signature( + { + "keyid": identity.urls.key.full(), # type:ignore + "headers": list(headers.keys()), + "signature": identity.sign(signed_string), + "algorithm": "rsa-sha256", + } + ) + del headers["(request-target)"] + async with httpx.AsyncClient() as client: + print(f"Calling {method} {uri}") + print(body) + return await client.request( + method, + uri, + headers=headers, + content=body_bytes, + ) + class SignatureDetails(TypedDict): algorithm: str -- cgit v1.2.3