Start the Litepub support

litepub
Thomas Sileo 2019-04-24 22:38:17 +02:00
rodzic 9724059d40
commit 04a68aaed9
5 zmienionych plików z 6 dodań i 158 usunięć

Wyświetl plik

@ -5,6 +5,8 @@
<a href="https://github.com/tsileo/little-boxes/blob/master/LICENSE"><img src="https://img.shields.io/badge/license-ISC-red.svg?style=flat" alt="License"></a>
<a href="https://github.com/ambv/black"><img alt="Code style: black" src="https://img.shields.io/badge/code%20style-black-000000.svg"></a>
/!\ [Litepub](https://litepub.social/litepub/) support in progress
Tiny [ActivityPub](https://activitypub.rocks/) framework written in Python, both database and server agnostic.
**Still in early development, and not published on PyPI yet.**
@ -23,45 +25,9 @@ Until a first version is released, the main goal of this framework is to power t
- with helpers for parsing hashtags and linkify content
- Key (RSA) helper
- HTTP signature helper
- JSON-LD signature helper
- Webfinger helper
## Getting Started
```python
from little_boxes import activitypub as ap
from mydb import db_client
class MyBackend(ap.Backend):
def __init__(self, db_connection):
self.db_connection = db_connection
def inbox_new(self, as_actor: ap.Person, activity: ap.Activity) -> None:
# Save activity as "as_actor"
# [...]
def post_to_remote_inbox(self, as_actor: ap.Person, payload: ap.ObjectType, recipient: str) -> None:
# Send the activity to the remote actor
# [...]
db_con = db_client()
my_backend = MyBackend(db_con)
ap.use_backend(my_backend)
me = ap.Person({}) # Init an actor
outbox = ap.Outbox(me)
follow = ap.Follow(actor=me.id, object='http://iri-i-want-follow')
outbox.post(follow)
```
## Projects using Little Boxes
- [microblog.pub](http://github.com/tsileo/microblog.pub) (using MongoDB as a backend)

Wyświetl plik

@ -31,25 +31,12 @@ ActorType = Union["Person", "Application", "Group", "Organization", "Service"]
ObjectOrIDType = Union[str, ObjectType]
CTX_AS = "https://www.w3.org/ns/activitystreams"
CTX_SECURITY = "https://w3id.org/security/v1"
AS_PUBLIC = "https://www.w3.org/ns/activitystreams#Public"
DEFAULT_CTX = COLLECTION_CTX = [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
{
# AS ext
"Hashtag": "as:Hashtag",
"sensitive": "as:sensitive",
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
# toot
"toot": "http://joinmastodon.org/ns#",
"featured": "toot:featured",
# schema
"schema": "http://schema.org#",
"PropertyValue": "schema:PropertyValue",
"value": "schema:value",
},
"https://pleroma.site/schemas/litepub-0.1.jsonld",
{"@language": "und"},
]
# Will be used to keep track of all the defined activities
@ -333,13 +320,12 @@ class BaseActivity(object, metaclass=_ActivityMeta):
# @context check
if not isinstance(self._data["@context"], list):
self._data["@context"] = [self._data["@context"]]
if CTX_SECURITY not in self._data["@context"]:
self._data["@context"].append(CTX_SECURITY)
if isinstance(self._data["@context"][-1], dict):
self._data["@context"][-1]["Hashtag"] = "as:Hashtag"
self._data["@context"][-1]["sensitive"] = "as:sensitive"
self._data["@context"][-1]["toot"] = "http://joinmastodon.org/ns#"
self._data["@context"][-1]["featured"] = "toot:featured"
self._data["@context"][-1]["@language"] = "und"
else:
self._data["@context"].append(
{
@ -347,6 +333,7 @@ class BaseActivity(object, metaclass=_ActivityMeta):
"sensitive": "as:sensitive",
"toot": "http://joinmastodon.org/ns#",
"featured": "toot:featured",
"@language": "und",
}
)

Wyświetl plik

@ -1,83 +0,0 @@
import base64
import hashlib
import typing
from datetime import datetime
from typing import Any
from typing import Dict
from Crypto.Hash import SHA256
from Crypto.Signature import PKCS1_v1_5
from pyld import jsonld
if typing.TYPE_CHECKING:
from .key import Key # noqa: type checking
# cache the downloaded "schemas", otherwise the library is super slow
# (https://github.com/digitalbazaar/pyld/issues/70)
_CACHE: Dict[str, Any] = {}
LOADER = jsonld.requests_document_loader()
def _caching_document_loader(url: str) -> Any:
if url in _CACHE:
return _CACHE[url]
resp = LOADER(url)
_CACHE[url] = resp
return resp
jsonld.set_document_loader(_caching_document_loader)
def _options_hash(doc):
doc = dict(doc["signature"])
for k in ["type", "id", "signatureValue"]:
if k in doc:
del doc[k]
doc["@context"] = "https://w3id.org/identity/v1"
normalized = jsonld.normalize(
doc, {"algorithm": "URDNA2015", "format": "application/nquads"}
)
h = hashlib.new("sha256")
h.update(normalized.encode("utf-8"))
return h.hexdigest()
def _doc_hash(doc):
doc = dict(doc)
if "signature" in doc:
del doc["signature"]
normalized = jsonld.normalize(
doc, {"algorithm": "URDNA2015", "format": "application/nquads"}
)
h = hashlib.new("sha256")
h.update(normalized.encode("utf-8"))
return h.hexdigest()
def verify_signature(doc, key: "Key"):
to_be_signed = _options_hash(doc) + _doc_hash(doc)
signature = doc["signature"]["signatureValue"]
signer = PKCS1_v1_5.new(key.pubkey or key.privkey) # type: ignore
digest = SHA256.new()
digest.update(to_be_signed.encode("utf-8"))
return signer.verify(digest, base64.b64decode(signature)) # type: ignore
def generate_signature(doc, key: "Key"):
options = {
"type": "RsaSignature2017",
"creator": doc["actor"] + "#main-key",
"created": datetime.utcnow().replace(microsecond=0).isoformat() + "Z",
}
doc["signature"] = options
to_be_signed = _options_hash(doc) + _doc_hash(doc)
if not key.privkey:
raise ValueError(f"missing privkey on key {key!r}")
signer = PKCS1_v1_5.new(key.privkey)
digest = SHA256.new()
digest.update(to_be_signed.encode("utf-8"))
sig = base64.b64encode(signer.sign(digest)) # type: ignore
options["signatureValue"] = sig.decode("utf-8")

Wyświetl plik

@ -26,7 +26,6 @@ REQUIRED = [
"requests",
"markdown",
"bleach",
"pyld",
"pycryptodome",
"html2text",
"mdx_linkify",
@ -70,7 +69,6 @@ setup(
"Development Status :: 3 - Alpha",
"License :: OSI Approved :: ISC License (ISCL)",
"Programming Language :: Python",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",

Wyświetl plik

@ -1,20 +0,0 @@
import json
import logging
from little_boxes import linked_data_sig
from little_boxes.key import Key
logging.basicConfig(level=logging.DEBUG)
DOC = """{"type": "Create", "actor": "https://microblog.pub", "object": {"type": "Note", "sensitive": false, "cc": ["https://microblog.pub/followers"], "to": ["https://www.w3.org/ns/activitystreams#Public"], "content": "<p>Hello world!</p>", "tag": [], "source": {"mediaType": "text/markdown", "content": "Hello world!"}, "attributedTo": "https://microblog.pub", "published": "2018-05-21T15:51:59Z", "id": "https://microblog.pub/outbox/988179f13c78b3a7/activity", "url": "https://microblog.pub/note/988179f13c78b3a7", "replies": {"type": "OrderedCollection", "totalItems": 0, "first": "https://microblog.pub/outbox/988179f13c78b3a7/replies?page=first", "id": "https://microblog.pub/outbox/988179f13c78b3a7/replies"}, "likes": {"type": "OrderedCollection", "totalItems": 2, "first": "https://microblog.pub/outbox/988179f13c78b3a7/likes?page=first", "id": "https://microblog.pub/outbox/988179f13c78b3a7/likes"}, "shares": {"type": "OrderedCollection", "totalItems": 3, "first": "https://microblog.pub/outbox/988179f13c78b3a7/shares?page=first", "id": "https://microblog.pub/outbox/988179f13c78b3a7/shares"}}, "@context": ["https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1", {"Hashtag": "as:Hashtag", "sensitive": "as:sensitive"}], "published": "2018-05-21T15:51:59Z", "to": ["https://www.w3.org/ns/activitystreams#Public"], "cc": ["https://microblog.pub/followers"], "id": "https://microblog.pub/outbox/988179f13c78b3a7"}""" # noqa: E501
def test_linked_data_sig():
doc = json.loads(DOC)
k = Key("https://lol.com")
k.new()
linked_data_sig.generate_signature(doc, k)
assert linked_data_sig.verify_signature(doc, k)