kopia lustrzana https://gitlab.com/jaywink/federation
rewrote update_context to make it (hopefully) more robust
added django federation user to enable http signature on get requestsjsonld-outbound
rodzic
4c18ce72f6
commit
7980f2c5cc
|
@ -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]
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Ładowanie…
Reference in New Issue