kopia lustrzana https://gitlab.com/jaywink/federation
Fixes to address the reviewer's comments. Where appropriate, align with existing code structure.
rodzic
26f93ec1be
commit
e7d954b788
|
@ -2,10 +2,12 @@ import json
|
|||
from datetime import timedelta
|
||||
from pyld import jsonld
|
||||
|
||||
from federation.utils.django import get_redis
|
||||
|
||||
cache = get_redis() or {}
|
||||
EXPIRATION = int(timedelta(weeks=4).total_seconds())
|
||||
try:
|
||||
from federation.utils.django import get_redis
|
||||
cache = get_redis() or {}
|
||||
EXPIRATION = int(timedelta(weeks=4).total_seconds())
|
||||
except:
|
||||
cache = {}
|
||||
|
||||
|
||||
# This is required to workaround a bug in pyld that has the Accept header
|
||||
|
@ -24,9 +26,9 @@ def get_loader(*args, **kwargs):
|
|||
options['headers']['Accept'] = 'application/ld+json'
|
||||
doc = requests_loader(url, options)
|
||||
if isinstance(cache, dict):
|
||||
cache[url] = json.dumps(doc)
|
||||
cache[key] = json.dumps(doc)
|
||||
else:
|
||||
cache.set(f'ld_cache:{url}', json.dumps(doc), ex=EXPIRATION)
|
||||
cache.set(key, json.dumps(doc), ex=EXPIRATION)
|
||||
return doc
|
||||
|
||||
return loader
|
||||
|
|
|
@ -6,6 +6,7 @@ from pyld import jsonld
|
|||
|
||||
from federation.entities.activitypub.constants import CONTEXT_ACTIVITYSTREAMS, CONTEXT_SECURITY, NAMESPACE_PUBLIC
|
||||
|
||||
|
||||
# Extract context information from the metadata parameter defined for fields
|
||||
# that are not part of the official AP spec. Use the same extended context for
|
||||
# inbound payload. For outbound payload, build a context with only the required
|
||||
|
@ -22,16 +23,16 @@ class LdContextManager:
|
|||
self._extensions[klass] = {}
|
||||
ctx = getattr(klass, 'ctx', [])
|
||||
if ctx:
|
||||
self._extensions[klass].update({klass.__name__:ctx})
|
||||
self._extensions[klass].update({klass.__name__: ctx})
|
||||
for name, value in klass.schema().declared_fields.items():
|
||||
ctx = value.metadata.get('ctx') or []
|
||||
if ctx:
|
||||
self._extensions[klass].update({name:ctx})
|
||||
self._extensions[klass].update({name: ctx})
|
||||
merged = {}
|
||||
for field in self._extensions.values():
|
||||
for ctx in field.values():
|
||||
self._add_extensions(ctx, self._named, merged)
|
||||
self._merged = copy.copy(self._named)
|
||||
self._merged = copy.copy(self._named)
|
||||
self._merged.append(merged)
|
||||
|
||||
def _add_extensions(self, field, named, extensions):
|
||||
|
@ -41,7 +42,6 @@ class LdContextManager:
|
|||
elif isinstance(item, dict):
|
||||
extensions.update(item)
|
||||
|
||||
|
||||
def _get_fields(self, obj):
|
||||
for klass in self._extensions.keys():
|
||||
if issubclass(type(obj), klass):
|
||||
|
@ -58,14 +58,16 @@ class LdContextManager:
|
|||
def patch_payload(payload, patched):
|
||||
for field in ('attachment', 'cc', 'tag', 'to'):
|
||||
value = payload.get(field)
|
||||
if value and not isinstance(value, list):
|
||||
if not value:
|
||||
continue
|
||||
if not isinstance(value, list):
|
||||
value = [value]
|
||||
patched[field] = value
|
||||
patched[field] = value
|
||||
if field in ('cc', 'to'):
|
||||
try:
|
||||
idx = value.index('as:Public')
|
||||
patched[field][idx] = value[idx].replace('as:Public', NAMESPACE_PUBLIC)
|
||||
except:
|
||||
except ValueError:
|
||||
pass
|
||||
if isinstance(payload.get('object'), dict):
|
||||
patch_payload(payload['object'], patched['object'])
|
||||
|
@ -88,22 +90,25 @@ class LdContextManager:
|
|||
if field in to_add.keys():
|
||||
if field_value is not missing or obj.signable and field == 'signature':
|
||||
self._add_extensions(to_add[field], final, extensions)
|
||||
if not isinstance(field_value, list): field_value = [field_value]
|
||||
if not isinstance(field_value, list):
|
||||
field_value = [field_value]
|
||||
for value in field_value:
|
||||
if issubclass(type(value), (Object, Link)):
|
||||
walk_object(value)
|
||||
|
||||
walk_object(obj)
|
||||
if extensions: final.append(extensions)
|
||||
if extensions:
|
||||
final.append(extensions)
|
||||
# compact the array if len == 1 to minimize test changes
|
||||
return final if len(final) > 1 else final[0]
|
||||
|
||||
def merge_context(self, ctx):
|
||||
# One platform sends a single string context
|
||||
if isinstance(ctx, str): ctx = [ctx]
|
||||
if isinstance(ctx, str):
|
||||
ctx = [ctx]
|
||||
|
||||
# add a # at the end of the python-federation string
|
||||
# for socialhome payloads
|
||||
# for legacy socialhome payloads
|
||||
s = json.dumps(ctx)
|
||||
if 'python-federation"' in s:
|
||||
ctx = json.loads(s.replace('python-federation', 'python-federation#', 1))
|
||||
|
@ -112,7 +117,7 @@ class LdContextManager:
|
|||
# is not a json-ld document.
|
||||
try:
|
||||
ctx.pop(ctx.index('http://joinmastodon.org/ns'))
|
||||
except:
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# remove @language in context since this directive is not
|
||||
|
@ -124,13 +129,12 @@ class LdContextManager:
|
|||
for i, v in enumerate(ctx):
|
||||
if isinstance(v, dict):
|
||||
v.pop('@language', None)
|
||||
if len(v) == 0: idx.insert(0, i)
|
||||
for i in idx: ctx.pop(i)
|
||||
if len(v) == 0:
|
||||
idx.insert(0, i)
|
||||
for i in idx:
|
||||
ctx.pop(i)
|
||||
|
||||
# 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
|
||||
# include all Mastodon extensions for platforms that only define http://joinmastodon.org/ns in their context
|
||||
# Merge all defined AP extensions to the inbound context
|
||||
uris = []
|
||||
defs = {}
|
||||
# Merge original context dicts in one dict
|
||||
|
|
|
@ -3,7 +3,7 @@ import logging
|
|||
import math
|
||||
import re
|
||||
from base64 import b64encode, b64decode
|
||||
from copy import copy
|
||||
from copy import copy
|
||||
from funcy import omit
|
||||
from pyld import jsonld
|
||||
|
||||
|
@ -22,13 +22,13 @@ def create_ld_signature(obj, author):
|
|||
sig = {
|
||||
'created': datetime.datetime.now(tz=datetime.timezone.utc).isoformat(timespec='seconds'),
|
||||
'creator': f'{author.id}#main-key',
|
||||
'@context':'https://w3id.org/security/v1'
|
||||
'@context': 'https://w3id.org/security/v1'
|
||||
}
|
||||
|
||||
try:
|
||||
private_key = import_key(author.private_key)
|
||||
except (ValueError, TypeError) as exc:
|
||||
logger.warning(f'ld_signature - {exc}')
|
||||
logger.warning('ld_signature - %s', exc)
|
||||
return None
|
||||
signer = pkcs1_15.new(private_key)
|
||||
|
||||
|
@ -40,7 +40,7 @@ def create_ld_signature(obj, author):
|
|||
sig.update({'type': 'RsaSignature2017', 'signatureValue': b64encode(signature).decode()})
|
||||
sig.pop('@context')
|
||||
|
||||
obj.update({'signature':sig})
|
||||
obj.update({'signature': sig})
|
||||
|
||||
|
||||
def verify_ld_signature(payload):
|
||||
|
@ -49,24 +49,25 @@ def verify_ld_signature(payload):
|
|||
"""
|
||||
signature = copy(payload.get('signature', None))
|
||||
if not signature:
|
||||
logger.warning(f'ld_signature - No signature in {payload.get("id", "the payload")}')
|
||||
logger.warning('ld_signature - No signature in %s', payload.get("id", "the payload"))
|
||||
return None
|
||||
|
||||
# retrieve the author's public key
|
||||
profile = retrieve_and_parse_document(signature.get('creator'))
|
||||
if not profile:
|
||||
logger.warning(f'ld_signature - Failed to retrieve profile for {signature.get("creator")}')
|
||||
|
||||
logger.warning('ld_signature - Failed to retrieve profile for %s', signature.get("creator"))
|
||||
return None
|
||||
try:
|
||||
pkey = import_key(profile.public_key)
|
||||
except ValueError as exc:
|
||||
logger.warning(f'ld_signature - {exc}')
|
||||
logger.warning('ld_signature - %s', exc)
|
||||
return None
|
||||
verifier = pkcs1_15.new(pkey)
|
||||
|
||||
# Compute digests and verify signature
|
||||
sig = omit(signature, ('type', 'signatureValue'))
|
||||
sig.update({'@context':'https://w3id.org/security/v1'})
|
||||
sig.update({'@context': 'https://w3id.org/security/v1'})
|
||||
sig_digest = hash(sig)
|
||||
obj = omit(payload, 'signature')
|
||||
obj_digest = hash(obj)
|
||||
|
@ -75,15 +76,15 @@ def verify_ld_signature(payload):
|
|||
sig_value = b64decode(signature.get('signatureValue'))
|
||||
try:
|
||||
verifier.verify(SHA256.new(digest), sig_value)
|
||||
logger.debug(f'ld_signature - {payload.get("id")} has a valid signature')
|
||||
logger.debug('ld_signature - %s has a valid signature', payload.get("id"))
|
||||
return profile.id
|
||||
except ValueError:
|
||||
logger.warning(f'ld_signature - Invalid signature for {payload.get("id")}')
|
||||
logger.warning('ld_signature - Invalid signature for %s', payload.get("id"))
|
||||
return None
|
||||
|
||||
|
||||
def hash(obj):
|
||||
nquads = NormalizedDoubles().normalize(obj, options={'format':'application/nquads','algorithm':'URDNA2015'})
|
||||
nquads = NormalizedDoubles().normalize(obj, options={'format': 'application/nquads', 'algorithm': 'URDNA2015'})
|
||||
return SHA256.new(nquads.encode('utf-8')).hexdigest()
|
||||
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ import bleach
|
|||
from calamus import fields
|
||||
from calamus.schema import JsonLDAnnotation, JsonLDSchema, JsonLDSchemaOpts
|
||||
from calamus.utils import normalize_value
|
||||
from cryptography.exceptions import InvalidSignature
|
||||
from marshmallow import exceptions, pre_load, post_load, post_dump
|
||||
from marshmallow.fields import Integer
|
||||
from marshmallow.utils import EXCLUDE, missing
|
||||
|
@ -25,7 +26,6 @@ from federation.outbound import handle_send
|
|||
from federation.types import UserType, ReceiverVariant
|
||||
from federation.utils.activitypub import retrieve_and_parse_document, retrieve_and_parse_profile, \
|
||||
get_profile_id_from_webfinger
|
||||
from federation.utils.django import get_configuration
|
||||
from federation.utils.text import with_slash, validate_handle
|
||||
|
||||
logger = logging.getLogger("federation")
|
||||
|
@ -297,12 +297,23 @@ class Object(BaseEntity, metaclass=JsonLDAnnotation):
|
|||
# TODO: rework validation
|
||||
def validate(self, direction='inbound'):
|
||||
if direction == 'inbound':
|
||||
# ensure marshmallow.missing is not sent to the client app
|
||||
for attr in type(self).schema().load_fields.keys():
|
||||
if getattr(self, attr) is missing:
|
||||
setattr(self, attr, None)
|
||||
|
||||
super().validate(direction)
|
||||
|
||||
def _validate_signatures(self):
|
||||
# Always verify the inbound LD signature, for monitoring purposes
|
||||
actor = verify_ld_signature(self._source_object)
|
||||
if not self._sender:
|
||||
return
|
||||
if self.signable and self._sender not in (self.id, getattr(self, 'actor_id', None)):
|
||||
# Relayed payload
|
||||
if not actor:
|
||||
raise InvalidSignature('no or invalid signature for %s, a relayed payload', self.id)
|
||||
|
||||
def to_string(self):
|
||||
# noinspection PyUnresolvedReferences
|
||||
return str(self.to_as2())
|
||||
|
@ -457,6 +468,7 @@ class Link(metaclass=JsonLDAnnotation):
|
|||
|
||||
class Hashtag(Link):
|
||||
ctx = [{'Hashtag': 'as:Hashtag'}]
|
||||
id = fields.Id() # Hubzilla uses id instead of href
|
||||
|
||||
class Meta:
|
||||
rdf_type = as2.Hashtag
|
||||
|
@ -702,7 +714,7 @@ class Note(Object, RawContentMixin):
|
|||
self._allowed_children += (base.Audio, base.Video)
|
||||
|
||||
def to_as2(self):
|
||||
#self.sensitive = 'nsfw' in self.tags
|
||||
self.sensitive = 'nsfw' in self.tags
|
||||
self.url = self.id
|
||||
|
||||
edited = False
|
||||
|
@ -768,7 +780,13 @@ class Note(Object, RawContentMixin):
|
|||
# Skip when markdown
|
||||
return
|
||||
|
||||
hrefs = [tag.href.lower() for tag in self.tag_objects if isinstance(tag, Hashtag)]
|
||||
hrefs = []
|
||||
for tag in self.tag_objects:
|
||||
if isinstance(tag, Hashtag):
|
||||
if tag.href is not missing:
|
||||
hrefs.append(tag.href.lower())
|
||||
elif tag.id is not missing:
|
||||
hrefs.append(tag.id.lower())
|
||||
# noinspection PyUnusedLocal
|
||||
def remove_tag_links(attrs, new=False):
|
||||
# Hashtag object hrefs
|
||||
|
@ -807,6 +825,7 @@ class Note(Object, RawContentMixin):
|
|||
Populate tags to the object.tag list.
|
||||
"""
|
||||
try:
|
||||
from federation.utils.django import get_configuration
|
||||
config = get_configuration()
|
||||
except ImportError:
|
||||
tags_path = None
|
||||
|
@ -1321,6 +1340,7 @@ def element_to_objects(element: Union[Dict, Object], sender: str = "") -> List:
|
|||
entity = model_to_objects(element) if not isinstance(element, Object) else element
|
||||
if entity and hasattr(entity, 'to_base'):
|
||||
entity = entity.to_base()
|
||||
entity._sender = sender
|
||||
if isinstance(entity, (
|
||||
base.Post, base.Comment, base.Profile, base.Share, base.Follow,
|
||||
base.Retraction, base.Accept,)
|
||||
|
@ -1330,13 +1350,11 @@ def element_to_objects(element: Union[Dict, Object], sender: str = "") -> List:
|
|||
except ValueError as ex:
|
||||
logger.error("Failed to validate entity %s: %s", entity, ex)
|
||||
return []
|
||||
# Always verify the LD signature, for monitoring purposes
|
||||
actor = verify_ld_signature(entity._source_object)
|
||||
if entity.signable and sender not in (entity.id, getattr(entity, 'actor_id', None), ''):
|
||||
# Relayed payload
|
||||
if not actor:
|
||||
logger.warning(f'no or invalid signature for a relayed payload, fetching {entity.id}')
|
||||
entity = retrieve_and_parse_document(entity.id)
|
||||
except InvalidSignature as exc:
|
||||
logger.info('%s, fetching from remote', exc)
|
||||
entity = retrieve_and_parse_document(entity.id)
|
||||
if not entity:
|
||||
return []
|
||||
logger.info('Entity type "%s" was handled through the json-ld processor', entity.__class__.__name__)
|
||||
return [entity]
|
||||
elif entity:
|
||||
|
@ -1355,7 +1373,7 @@ def model_to_objects(payload):
|
|||
try:
|
||||
entity = model.schema().load(payload)
|
||||
except (KeyError, jsonld.JsonLdError, exceptions.ValidationError) as exc : # Just give up for now. This must be made robust
|
||||
logger.error(f"Error parsing jsonld payload ({exc})")
|
||||
logger.error("Error parsing jsonld payload (%s)", exc)
|
||||
return None
|
||||
|
||||
if isinstance(getattr(entity, 'object_', None), Object):
|
||||
|
|
|
@ -20,6 +20,7 @@ class BaseEntity:
|
|||
_source_protocol: str = ""
|
||||
# Contains the original object from payload as a string
|
||||
_source_object: Union[str, Dict] = None
|
||||
_sender: str = ""
|
||||
_sender_key: str = ""
|
||||
# ActivityType
|
||||
activity: ActivityType = None
|
||||
|
@ -231,8 +232,8 @@ class RawContentMixin(BaseEntity):
|
|||
@property
|
||||
def rendered_content(self) -> str:
|
||||
"""Returns the rendered version of raw_content, or just raw_content."""
|
||||
from federation.utils.django import get_configuration
|
||||
try:
|
||||
from federation.utils.django import get_configuration
|
||||
config = get_configuration()
|
||||
if config["tags_path"]:
|
||||
def linkifier(tag: str) -> str:
|
||||
|
|
|
@ -60,7 +60,7 @@ def rfc7033_webfinger_view(request, *args, **kwargs):
|
|||
if not resource.startswith("acct:"):
|
||||
return HttpResponseBadRequest("Invalid resource")
|
||||
handle = resource.replace("acct:", "").lower()
|
||||
logger.debug(f"{handle} requested with {request}")
|
||||
logger.debug("%s requested with %s", handle, request)
|
||||
profile_func = get_function_from_config("get_profile_function")
|
||||
|
||||
try:
|
||||
|
|
|
@ -90,6 +90,6 @@ class Protocol:
|
|||
# Verify the HTTP signature
|
||||
self.sender = verify_request_signature(self.request)
|
||||
except (ValueError, KeyError, InvalidSignature) as exc:
|
||||
logger.warning(f'HTTP signature verification failed: {exc}')
|
||||
logger.warning('HTTP signature verification failed: %s', exc)
|
||||
return self.actor, {}
|
||||
return self.sender, self.payload
|
||||
|
|
|
@ -79,9 +79,3 @@ def verify_request_signature(request: RequestType, required: bool=True):
|
|||
raise ValueError("Invalid signature")
|
||||
|
||||
return signer.id
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -3,14 +3,17 @@ import logging
|
|||
from typing import Optional, Any
|
||||
|
||||
from federation.protocols.activitypub.signing import get_http_authentication
|
||||
from federation.utils.django import get_federation_user
|
||||
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')
|
||||
|
||||
federation_user = get_federation_user()
|
||||
if not federation_user: logger.warning("django is required for get requests signing")
|
||||
try:
|
||||
from federation.utils.django import get_federation_user
|
||||
federation_user = get_federation_user()
|
||||
except ImportError:
|
||||
federation_user = None
|
||||
logger.warning("django is required for get requests signing")
|
||||
|
||||
|
||||
def get_profile_id_from_webfinger(handle: str) -> Optional[str]:
|
||||
|
|
Ładowanie…
Reference in New Issue