2019-03-03 01:09:25 +00:00
|
|
|
import importlib
|
2018-02-18 21:34:02 +00:00
|
|
|
import json
|
2018-02-18 21:01:48 +00:00
|
|
|
import logging
|
2019-06-21 22:31:50 +00:00
|
|
|
from typing import List, Dict, Union
|
2018-07-31 20:30:21 +00:00
|
|
|
|
2019-08-11 20:21:47 +00:00
|
|
|
# noinspection PyPackageRequirements
|
2018-08-11 20:56:46 +00:00
|
|
|
from Crypto.PublicKey.RSA import RsaKey
|
2019-03-17 17:39:55 +00:00
|
|
|
from iteration_utilities import unique_everseen
|
2018-02-18 21:01:48 +00:00
|
|
|
|
2019-05-12 20:30:35 +00:00
|
|
|
from federation.entities.activitypub.constants import NAMESPACE_PUBLIC
|
2018-07-31 20:30:21 +00:00
|
|
|
from federation.entities.mixins import BaseEntity
|
2019-03-17 01:18:07 +00:00
|
|
|
from federation.protocols.activitypub.signing import get_http_authentication
|
2018-07-31 20:30:21 +00:00
|
|
|
from federation.types import UserType
|
2017-05-06 21:20:57 +00:00
|
|
|
from federation.utils.network import send_document
|
2016-07-23 10:37:56 +00:00
|
|
|
|
2018-02-18 21:01:48 +00:00
|
|
|
logger = logging.getLogger("federation")
|
|
|
|
|
2016-07-23 10:37:56 +00:00
|
|
|
|
2018-07-31 20:30:21 +00:00
|
|
|
def handle_create_payload(
|
2019-03-03 01:09:25 +00:00
|
|
|
entity: BaseEntity,
|
|
|
|
author_user: UserType,
|
|
|
|
protocol_name: str,
|
|
|
|
to_user_key: RsaKey = None,
|
|
|
|
parent_user: UserType = None,
|
2019-06-21 22:31:50 +00:00
|
|
|
) -> Union[str, dict]:
|
2019-03-03 01:09:25 +00:00
|
|
|
"""Create a payload with the given protocol.
|
2016-07-23 10:37:56 +00:00
|
|
|
|
2017-07-28 20:22:32 +00:00
|
|
|
Any given user arguments must have ``private_key`` and ``handle`` attributes.
|
2016-07-23 10:37:56 +00:00
|
|
|
|
2017-07-28 20:22:32 +00:00
|
|
|
:arg entity: Entity object to send. Can be a base entity or a protocol specific one.
|
|
|
|
:arg author_user: User authoring the object.
|
2019-03-03 01:09:25 +00:00
|
|
|
:arg protocol_name: Protocol to create payload for.
|
2018-02-10 21:40:52 +00:00
|
|
|
:arg to_user_key: Public key of user private payload is being sent to, required for private payloads.
|
2017-07-28 20:22:32 +00:00
|
|
|
:arg parent_user: (Optional) User object of the parent object, if there is one. This must be given for the
|
|
|
|
Diaspora protocol if a parent object exists, so that a proper ``parent_author_signature`` can
|
|
|
|
be generated. If given, the payload will be sent as this user.
|
2016-10-02 10:08:37 +00:00
|
|
|
:returns: Built payload message (str)
|
2016-07-23 10:37:56 +00:00
|
|
|
"""
|
2019-03-03 01:09:25 +00:00
|
|
|
mappers = importlib.import_module(f"federation.entities.{protocol_name}.mappers")
|
|
|
|
protocol = importlib.import_module(f"federation.protocols.{protocol_name}.protocol")
|
|
|
|
protocol = protocol.Protocol()
|
2019-08-29 19:50:57 +00:00
|
|
|
outbound_entity = mappers.get_outbound_entity(entity, author_user.rsa_private_key)
|
2017-07-28 20:22:32 +00:00
|
|
|
if parent_user:
|
2019-08-29 19:50:57 +00:00
|
|
|
outbound_entity.sign_with_parent(parent_user.rsa_private_key)
|
2017-07-28 20:22:32 +00:00
|
|
|
send_as_user = parent_user if parent_user else author_user
|
2018-02-10 21:40:52 +00:00
|
|
|
data = protocol.build_send(entity=outbound_entity, from_user=send_as_user, to_user_key=to_user_key)
|
2016-07-23 10:37:56 +00:00
|
|
|
return data
|
2017-05-06 21:20:57 +00:00
|
|
|
|
|
|
|
|
2018-07-31 20:30:21 +00:00
|
|
|
def handle_send(
|
|
|
|
entity: BaseEntity,
|
|
|
|
author_user: UserType,
|
2019-03-17 17:39:55 +00:00
|
|
|
recipients: List[Dict],
|
2019-03-03 01:09:25 +00:00
|
|
|
parent_user: UserType = None,
|
2018-07-31 20:30:21 +00:00
|
|
|
) -> None:
|
2017-05-06 21:20:57 +00:00
|
|
|
"""Send an entity to remote servers.
|
|
|
|
|
2019-03-17 17:39:55 +00:00
|
|
|
Using this we will build a list of payloads per protocol. After that, each recipient will get the generated
|
|
|
|
protocol payload delivered. Delivery to the same endpoint will only be done once so it's ok to include
|
|
|
|
the same endpoint as a receiver multiple times.
|
2017-05-06 21:20:57 +00:00
|
|
|
|
2019-03-17 17:39:55 +00:00
|
|
|
Any given user arguments must have ``private_key`` and ``fid`` attributes.
|
2017-07-28 20:22:32 +00:00
|
|
|
|
|
|
|
:arg entity: Entity object to send. Can be a base entity or a protocol specific one.
|
|
|
|
:arg author_user: User authoring the object.
|
2019-03-17 17:39:55 +00:00
|
|
|
:arg recipients: A list of recipients to delivery to. Each recipient is a dict
|
2019-06-20 22:33:16 +00:00
|
|
|
containing at minimum the "endpoint", "fid", "public" and "protocol" keys.
|
2019-03-17 17:39:55 +00:00
|
|
|
|
2019-06-20 22:33:16 +00:00
|
|
|
For ActivityPub and Diaspora payloads, "endpoint" should be an URL of the endpoint to deliver to.
|
|
|
|
|
|
|
|
The "fid" can be empty for Diaspora payloads. For ActivityPub it should be the recipient
|
|
|
|
federation ID should the delivery be non-private.
|
2019-03-17 17:39:55 +00:00
|
|
|
|
|
|
|
The "protocol" should be a protocol name that is known for this recipient.
|
|
|
|
|
|
|
|
The "public" value should be a boolean to indicate whether the payload should be flagged as a
|
|
|
|
public payload.
|
|
|
|
|
|
|
|
TODO: support guessing the protocol over networks? Would need caching of results
|
|
|
|
|
|
|
|
For private deliveries to Diaspora protocol recipients, "public_key" is also required.
|
|
|
|
|
2018-02-10 21:40:52 +00:00
|
|
|
For example
|
|
|
|
[
|
2019-03-17 17:39:55 +00:00
|
|
|
{
|
2019-06-20 22:33:16 +00:00
|
|
|
"endpoint": "https://domain.tld/receive/users/1234-5678-0123-4567",
|
|
|
|
"fid": "",
|
2019-03-17 17:39:55 +00:00
|
|
|
"protocol": "diaspora",
|
|
|
|
"public": False,
|
|
|
|
"public_key": <RSAPublicKey object>,
|
|
|
|
},
|
|
|
|
{
|
2019-06-20 22:33:16 +00:00
|
|
|
"endpoint": "https://domain2.tld/receive/public",
|
|
|
|
"fid": "",
|
2019-03-17 17:39:55 +00:00
|
|
|
"protocol": "diaspora",
|
|
|
|
"public": True,
|
|
|
|
},
|
|
|
|
{
|
2019-06-20 22:33:16 +00:00
|
|
|
"endpoint": "https://domain4.tld/sharedinbox/",
|
|
|
|
"fid": "https://domain4.tld/profiles/jack/",
|
2019-03-17 17:39:55 +00:00
|
|
|
"protocol": "activitypub",
|
|
|
|
"public": True,
|
|
|
|
},
|
|
|
|
{
|
2019-06-20 22:33:16 +00:00
|
|
|
"endpoint": "https://domain4.tld/profiles/jill/inbox",
|
2019-03-17 17:39:55 +00:00
|
|
|
"fid": "https://domain4.tld/profiles/jill",
|
|
|
|
"protocol": "activitypub",
|
|
|
|
"public": False,
|
|
|
|
},
|
2018-02-10 21:40:52 +00:00
|
|
|
]
|
2017-07-28 20:22:32 +00:00
|
|
|
:arg parent_user: (Optional) User object of the parent object, if there is one. This must be given for the
|
|
|
|
Diaspora protocol if a parent object exists, so that a proper ``parent_author_signature`` can
|
|
|
|
be generated. If given, the payload will be sent as this user.
|
2017-05-06 21:20:57 +00:00
|
|
|
"""
|
2018-02-10 21:40:52 +00:00
|
|
|
payloads = []
|
|
|
|
public_payloads = {
|
2019-03-03 01:09:25 +00:00
|
|
|
"activitypub": {
|
2019-03-17 01:18:07 +00:00
|
|
|
"auth": None,
|
2019-03-03 01:09:25 +00:00
|
|
|
"payload": None,
|
|
|
|
"urls": set(),
|
|
|
|
},
|
2018-02-10 21:40:52 +00:00
|
|
|
"diaspora": {
|
2019-03-17 01:18:07 +00:00
|
|
|
"auth": None,
|
2018-02-10 21:40:52 +00:00
|
|
|
"payload": None,
|
|
|
|
"urls": set(),
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
2019-03-17 17:39:55 +00:00
|
|
|
# Flatten to unique recipients
|
2019-06-20 22:33:16 +00:00
|
|
|
# TODO supply a callable that empties "fid" in the case that public=True
|
2019-03-17 17:39:55 +00:00
|
|
|
unique_recipients = unique_everseen(recipients)
|
|
|
|
|
2018-02-10 21:40:52 +00:00
|
|
|
# Generate payloads and collect urls
|
2019-03-17 17:39:55 +00:00
|
|
|
for recipient in unique_recipients:
|
2019-06-20 22:33:16 +00:00
|
|
|
endpoint = recipient["endpoint"]
|
2019-03-17 17:39:55 +00:00
|
|
|
fid = recipient["fid"]
|
|
|
|
public_key = recipient.get("public_key")
|
|
|
|
protocol = recipient["protocol"]
|
|
|
|
public = recipient["public"]
|
|
|
|
|
|
|
|
if protocol == "activitypub":
|
|
|
|
try:
|
|
|
|
payload = handle_create_payload(entity, author_user, protocol, parent_user=parent_user)
|
|
|
|
if public:
|
2019-08-10 22:29:30 +00:00
|
|
|
payload["to"] = [NAMESPACE_PUBLIC]
|
2019-06-21 23:15:24 +00:00
|
|
|
payload["cc"] = [fid]
|
2019-06-21 22:31:50 +00:00
|
|
|
if isinstance(payload.get("object"), dict):
|
2019-08-10 22:29:30 +00:00
|
|
|
payload["object"]["to"] = [NAMESPACE_PUBLIC]
|
2019-06-21 23:15:24 +00:00
|
|
|
payload["object"]["cc"] = [fid]
|
2019-03-17 17:39:55 +00:00
|
|
|
else:
|
2019-08-10 22:29:30 +00:00
|
|
|
payload["to"] = [fid]
|
2019-06-21 22:31:50 +00:00
|
|
|
if isinstance(payload.get("object"), dict):
|
2019-08-10 22:29:30 +00:00
|
|
|
payload["object"]["to"] = [fid]
|
2019-03-17 17:39:55 +00:00
|
|
|
payload = json.dumps(payload).encode("utf-8")
|
|
|
|
except Exception as ex:
|
2019-08-18 00:20:35 +00:00
|
|
|
logger.error("handle_send - failed to generate payload for %s, %s: %s", fid, endpoint, ex)
|
2019-03-17 17:39:55 +00:00
|
|
|
continue
|
|
|
|
payloads.append({
|
2019-08-29 19:50:57 +00:00
|
|
|
"auth": get_http_authentication(author_user.rsa_private_key, f"{author_user.id}#main-key"),
|
2019-03-17 17:39:55 +00:00
|
|
|
"payload": payload,
|
|
|
|
"content_type": 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
|
2019-06-20 22:33:16 +00:00
|
|
|
"urls": {endpoint},
|
2019-03-17 17:39:55 +00:00
|
|
|
})
|
|
|
|
elif protocol == "diaspora":
|
|
|
|
if public:
|
|
|
|
if public_key:
|
|
|
|
raise ValueError("handle_send - Diaspora recipient cannot be public and use encrypted delivery")
|
|
|
|
if not public_payloads[protocol]["payload"]:
|
2019-08-11 20:21:47 +00:00
|
|
|
try:
|
|
|
|
# noinspection PyTypeChecker
|
|
|
|
public_payloads[protocol]["payload"] = handle_create_payload(
|
|
|
|
entity, author_user, protocol, parent_user=parent_user,
|
|
|
|
)
|
|
|
|
except Exception as ex:
|
|
|
|
logger.error("handle_send - failed to generate public payload for %s: %s", endpoint, ex)
|
2019-06-20 22:33:16 +00:00
|
|
|
public_payloads["diaspora"]["urls"].add(endpoint)
|
2019-03-17 17:39:55 +00:00
|
|
|
else:
|
|
|
|
if not public_key:
|
|
|
|
raise ValueError("handle_send - Diaspora recipient cannot be private without a public key for "
|
|
|
|
"encrypted delivery")
|
|
|
|
# Private payload
|
2019-03-03 01:09:25 +00:00
|
|
|
try:
|
|
|
|
payload = handle_create_payload(
|
|
|
|
entity, author_user, "diaspora", to_user_key=public_key, parent_user=parent_user,
|
|
|
|
)
|
|
|
|
payload = json.dumps(payload)
|
|
|
|
except Exception as ex:
|
2019-08-11 20:21:47 +00:00
|
|
|
logger.error("handle_send - failed to generate private payload for %s: %s", endpoint, ex)
|
2019-03-03 01:09:25 +00:00
|
|
|
continue
|
|
|
|
payloads.append({
|
2019-06-20 22:33:16 +00:00
|
|
|
"urls": {endpoint}, "payload": payload, "content_type": "application/json", "auth": None,
|
2019-03-03 01:09:25 +00:00
|
|
|
})
|
2019-03-17 17:39:55 +00:00
|
|
|
|
|
|
|
# Add public diaspora payload
|
2018-02-10 21:40:52 +00:00
|
|
|
if public_payloads["diaspora"]["payload"]:
|
|
|
|
payloads.append({
|
|
|
|
"urls": public_payloads["diaspora"]["urls"], "payload": public_payloads["diaspora"]["payload"],
|
2019-03-17 01:18:07 +00:00
|
|
|
"content_type": "application/magic-envelope+xml", "auth": None,
|
2018-02-10 21:40:52 +00:00
|
|
|
})
|
|
|
|
|
2018-02-18 21:01:48 +00:00
|
|
|
logger.debug("handle_send - %s", payloads)
|
|
|
|
|
2017-05-06 21:20:57 +00:00
|
|
|
# Do actual sending
|
2018-02-10 21:40:52 +00:00
|
|
|
for payload in payloads:
|
|
|
|
for url in payload["urls"]:
|
2018-02-18 21:34:02 +00:00
|
|
|
try:
|
2019-03-17 01:18:07 +00:00
|
|
|
send_document(
|
|
|
|
url,
|
|
|
|
payload["payload"],
|
|
|
|
auth=payload["auth"],
|
|
|
|
headers={"Content-Type": payload["content_type"]},
|
|
|
|
)
|
2018-02-18 21:34:02 +00:00
|
|
|
except Exception as ex:
|
|
|
|
logger.error("handle_send - failed to send payload to %s: %s, payload: %s", url, ex, payload["payload"])
|