rewrote update_context to make it (hopefully) more robust

added django federation user to enable http signature on get requests
jsonld-outbound
Alain St-Denis 2022-04-24 19:14:55 +00:00
rodzic 4c18ce72f6
commit 7980f2c5cc
5 zmienionych plików z 68 dodań i 30 usunięć

Wyświetl plik

@ -64,6 +64,13 @@ def element_to_objects(payload: Dict) -> List:
entity._receivers = extract_receivers(payload)
if hasattr(entity, "post_receive"):
entity.post_receive()
try:
entity.validate()
except ValueError as ex:
logger.error("Failed to validate entity %s: %s", entity, ex, extra={
"transformed": transformed,
})
return []
if hasattr(entity, "extract_mentions"):
entity.extract_mentions()
return [entity]

Wyświetl plik

@ -233,49 +233,49 @@ class Object(metaclass=JsonLDAnnotation):
@pre_load
def update_context(self, data, **kwargs):
if not data.get('@context'): return data
ctx = data['@context']
ctx = copy(data['@context'])
# add a # at the end of the python-federation string
# for socialhome payloads
s = json.dumps(ctx)
if 'python-federation"' in s:
ctx = json.loads(s.replace('python-federation', 'python-federation#', 1))
data['@context'] = ctx
# AP activities may be signed, but most platforms don't
# define RsaSignature2017. add it to the context
# hubzilla doesn't define the discoverable property in its context
to_add = {'signature': ['https://w3id.org/security/v1', {'sec':'https://w3id.org/security#','RsaSignature2017':'sec:RsaSignature2017'}],
may_add = {'signature': ['https://w3id.org/security/v1', {'sec':'https://w3id.org/security#','RsaSignature2017':'sec:RsaSignature2017'}],
'discoverable': [{'toot':'http://joinmastodon.org/ns#','discoverable': 'toot:discoverable'}], #for hubzilla
'copiedTo': [{'toot':'http://joinmastodon.org/ns#','copiedTo': 'toot:copiedTo'}], #for hubzilla
'featured': [{'toot':'http://joinmastodon.org/ns#','featured': 'toot:featured'}] #for litepub
}
if not isinstance(ctx, list):
ctx = [ctx, {}]
idx = [i for i,v in enumerate(ctx) if isinstance(v, dict)]
if len(idx) == 0:
ctx.append({})
idx = [len(ctx)-1]
saved_ctx = copy(ctx)
to_add = [val for key,val in may_add.items() if data.get(key)]
if to_add:
idx = [i for i,v in enumerate(ctx) if isinstance(v, dict)]
if idx:
upd = ctx[idx[0]]
# merge context dicts
if len(idx) > 1:
idx.reverse()
for i in idx[:-1]:
upd.update(ctx[i])
ctx.pop(i)
else:
upd = {}
for key,val in to_add.items():
if not data.get(key): continue
for item in val:
if isinstance(item, str) and item not in ctx:
ctx.append(item)
elif isinstance(item, dict):
for akey, aval in item.items():
found = False
for i in idx:
if ctx[i].get(aval):
found = True
break
if not found:
ctx[idx[0]][akey] = aval
if saved_ctx != ctx:
data['@context'] = ctx
for add in to_add:
for val in add:
if isinstance(val, str) and val not in ctx:
try:
ctx.append(val)
except AttributeError:
ctx = [ctx, val]
if isinstance(val, dict):
upd.update(val)
if not idx and upd: ctx.append(upd)
data['@context'] = ctx
return data
# A node without an id isn't true json-ld, but many payloads have
@ -503,6 +503,8 @@ class Person(Object):
'large': self.icon[0].url
}
entity._allowed_children += (PropertyValue, IdentityProof)
set_public(entity)
return entity
@ -640,6 +642,7 @@ class Announce(Activity):
target_id = IRI(as2.object)
def to_base(self):
if self.activity == self:
entity = ActivitypubShare(**self.__dict__)
else:
@ -695,6 +698,7 @@ class Delete(Create):
def to_base(self):
if hasattr(self, 'object_') and not isinstance(self.object_, Tombstone):
self.target_id = self.object_
self.entity_type = 'Object'
return ActivitypubRetraction(**self.__dict__)
class Meta:

Wyświetl plik

@ -4,11 +4,18 @@ from typing import Optional, Any
from federation.entities.activitypub.entities import ActivitypubProfile
from federation.entities.activitypub.mappers import message_to_objects
from federation.protocols.activitypub.signing import get_http_authentication
from federation.utils.network import fetch_document, try_retrieve_webfinger_document
from federation.utils.text import decode_if_bytes, validate_handle
logger = logging.getLogger('federation')
try:
from federation.utils.django import get_admin_user
admin_user = get_admin_user()
except ImportError:
admin_user = None
logger.warning("django is required for requests signing")
def get_profile_id_from_webfinger(handle: str) -> Optional[str]:
"""
@ -36,7 +43,9 @@ def retrieve_and_parse_document(fid: str) -> Optional[Any]:
"""
Retrieve remote document by ID and return the entity.
"""
document, status_code, ex = fetch_document(fid, extra_headers={'accept': 'application/activity+json'})
document, status_code, ex = fetch_document(fid,
extra_headers={'accept': 'application/activity+json'},
auth=get_http_authentication(admin_user.rsa_private_key,f'{admin_user.id}#main-key') if admin_user else None)
if document:
document = json.loads(decode_if_bytes(document))
entities = message_to_objects(document, fid)

Wyświetl plik

@ -2,6 +2,7 @@ import importlib
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from federation.types import UserType
def get_configuration():
@ -27,6 +28,7 @@ def get_configuration():
"get_private_key_function" in configuration,
"get_profile_function" in configuration,
"base_url" in configuration,
"federation_id" in configuration,
]):
raise ImproperlyConfigured("Missing required FEDERATION settings, please check documentation.")
return configuration
@ -42,3 +44,18 @@ def get_function_from_config(item):
module = importlib.import_module(module_path)
func = getattr(module, func_name)
return func
def get_admin_user():
config = get_configuration()
if not config.get('federation_id'): return None
try:
get_key = get_function_from_config("get_private_key_function")
except AttributeError:
return None
key = get_key(config['federation_id'])
if not key: return None
return UserType(id=config['federation_id'], private_key=key)

Wyświetl plik

@ -31,7 +31,7 @@ def fetch_content_type(url: str) -> Optional[str]:
return response.headers.get('Content-Type')
def fetch_document(url=None, host=None, path="/", timeout=10, raise_ssl_errors=True, extra_headers=None):
def fetch_document(url=None, host=None, path="/", timeout=10, raise_ssl_errors=True, extra_headers=None, **kwargs):
"""Helper method to fetch remote document.
Must be given either the ``url`` or ``host``.
@ -44,6 +44,7 @@ def fetch_document(url=None, host=None, path="/", timeout=10, raise_ssl_errors=T
:arg timeout: Seconds to wait for response (defaults to 10)
:arg raise_ssl_errors: Pass False if you want to try HTTP even for sites with SSL errors (default True)
:arg extra_headers: Optional extra headers dictionary to add to requests
:arg kwargs holds extra args passed to requests.get
:returns: Tuple of document (str or None), status code (int or None) and error (an exception class instance or None)
:raises ValueError: If neither url nor host are given as parameters
"""
@ -59,7 +60,7 @@ def fetch_document(url=None, host=None, path="/", timeout=10, raise_ssl_errors=T
# Use url since it was given
logger.debug("fetch_document: trying %s", url)
try:
response = requests.get(url, timeout=timeout, headers=headers)
response = requests.get(url, timeout=timeout, headers=headers, **kwargs)
logger.debug("fetch_document: found document, code %s", response.status_code)
response.raise_for_status()
return response.text, response.status_code, None