kopia lustrzana https://gitlab.com/jaywink/federation
New high level fetcher function retrieve_remote_content
The given ID will be fetched using the correct entity class specific remote endpoint, validated to be from the correct author against their public key and then an instance of the entity class will be constructed and returned. Also related changes and refactoring: * New Diaspora protocol helper `federation.utils.diaspora.retrieve_and_parse_content`. See notes regarding the high level fetcher above. * New Diaspora protocol helper `federation.utils.fetch_public_key`. Given a `handle` as a parameter, will fetch the remote profile and return the `public_key` from it. * Refactoring for Diaspora `MagicEnvelope` class. * Diaspora procotol receive flow now uses the `MagicEnvelope` class to verify payloads. * Diaspora protocol receive flow now fetches the sender public key over the network if a `sender_key_fetcher` function is not passed in. Previously an error would be raised. Closes #103merge-requests/130/head
rodzic
bcc779e006
commit
e343369f5b
25
CHANGELOG.md
25
CHANGELOG.md
|
@ -6,6 +6,31 @@
|
|||
* Added base entity `Share` which maps to a `DiasporaReshare` for the Diaspora protocol. ([related issue](https://github.com/jaywink/federation/issues/94))
|
||||
|
||||
The `Share` entity supports all the properties that a Diaspora reshare does. Additionally two other properties are supported: `raw_content` and `entity_type`. The former can be used for a "quoted share" case where the sharer adds their own note to the share. The latter can be used to reference the type of object that was shared, to help the receiver, if it is not sharing a `Post` entity. The value must be a base entity class name.
|
||||
|
||||
* New high level fetcher function `federation.fetchers.retrieve_remote_content`. ([related issue](https://github.com/jaywink/federation/issues/103))
|
||||
|
||||
This function takes the following parameters:
|
||||
|
||||
* `entity_class` - Base entity class to fetch (for example `Post`).
|
||||
* `id` - Object ID. Currently since only Diaspora is supported and the ID is expected to be in format `<guid>@<domain.tld>`.
|
||||
* `sender_key_fetcher` - Optional function that takes a profile `handle` and returns a public key in `str` format. If this is not given, the public key will be fetched from the remote profile over the network.
|
||||
|
||||
The given ID will be fetched using the correct entity class specific remote endpoint, validated to be from the correct author against their public key and then an instance of the entity class will be constructed and returned.
|
||||
|
||||
* New Diaspora protocol helper `federation.utils.diaspora.retrieve_and_parse_content`. See notes regarding the high level fetcher above.
|
||||
|
||||
* New Diaspora protocol helper `federation.utils.fetch_public_key`. Given a `handle` as a parameter, will fetch the remote profile and return the `public_key` from it.
|
||||
|
||||
### Changed
|
||||
* Refactoring for Diaspora `MagicEnvelope` class.
|
||||
|
||||
The class init now also allows passing in parameters to construct and verify MagicEnvelope instances. The order of init parameters has not been changed, but they are now all optional. When creating a class instance, one should always pass in the necessary parameters depnding on whether the class instance will be used for building a payload or verifying an incoming payload. See class docstring for details.
|
||||
|
||||
* Diaspora procotol receive flow now uses the `MagicEnvelope` class to verify payloads.
|
||||
|
||||
* Diaspora protocol receive flow now fetches the sender public key over the network if a `sender_key_fetcher` function is not passed in. Previously an error would be raised.
|
||||
|
||||
Note that fetching over the network for each payload is wasteful. Implementers should instead cache public keys when possible and pass in a function to retrieve them, as before.
|
||||
|
||||
### Fixed
|
||||
* Converting base entity `Profile` to `DiasporaProfile` for outbound sending missed two attributes, `image_urls` and `tag_list`. Those are now included so that the values transfer into the built payload.
|
||||
|
|
|
@ -66,6 +66,7 @@ Fetchers
|
|||
|
||||
High level utility functions to fetch remote objects. These should be favoured instead of protocol specific utility functions.
|
||||
|
||||
.. autofunction:: federation.fetchers.retrieve_remote_content
|
||||
.. autofunction:: federation.fetchers.retrieve_remote_profile
|
||||
|
||||
|
||||
|
@ -105,7 +106,11 @@ Various utils are provided for internal and external usage.
|
|||
Diaspora
|
||||
........
|
||||
|
||||
.. autofunction:: federation.utils.diaspora.fetch_public_key
|
||||
.. autofunction:: federation.utils.diaspora.get_fetch_content_endpoint
|
||||
.. autofunction:: federation.utils.diaspora.get_public_endpoint
|
||||
.. autofunction:: federation.utils.diaspora.parse_profile_from_hcard
|
||||
.. autofunction:: federation.utils.diaspora.retrieve_and_parse_content
|
||||
.. autofunction:: federation.utils.diaspora.retrieve_and_parse_profile
|
||||
.. autofunction:: federation.utils.diaspora.retrieve_diaspora_hcard
|
||||
.. autofunction:: federation.utils.diaspora.retrieve_diaspora_webfinger
|
||||
|
|
|
@ -12,6 +12,18 @@ from federation.utils.diaspora import retrieve_and_parse_profile
|
|||
|
||||
logger = logging.getLogger("federation")
|
||||
|
||||
BASE_MAPPINGS = {
|
||||
Comment: "comment",
|
||||
Follow: "contact",
|
||||
Image: "photo",
|
||||
Post: "status_message",
|
||||
Profile: "profile",
|
||||
Reaction: "like",
|
||||
Relationship: "request",
|
||||
Retraction: "retraction",
|
||||
Share: "reshare",
|
||||
}
|
||||
|
||||
MAPPINGS = {
|
||||
"status_message": DiasporaPost,
|
||||
"photo": Image,
|
||||
|
|
|
@ -1,7 +1,22 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import importlib
|
||||
|
||||
|
||||
def retrieve_remote_content(entity_class, id, sender_key_fetcher=None):
|
||||
"""Retrieve remote content and return an Entity object.
|
||||
|
||||
Currently, due to no other protocols supported, always use the Diaspora protocol.
|
||||
|
||||
:param entity_class: Federation entity class (from ``federation.entity.base``).
|
||||
:param id: ID of the remote entity, in format``guid@domain.tld``.
|
||||
:param sender_key_fetcher: Function to use to fetch sender public key. If not given, network will be used
|
||||
to fetch the profile and the key. Function must take handle as only parameter and return a public key.
|
||||
:returns: Entity class instance or ``None``
|
||||
"""
|
||||
protocol_name = "diaspora"
|
||||
utils = importlib.import_module("federation.utils.%s" % protocol_name)
|
||||
return utils.retrieve_and_parse_content(entity_class, id, sender_key_fetcher=sender_key_fetcher)
|
||||
|
||||
|
||||
def retrieve_remote_profile(handle):
|
||||
"""High level retrieve profile method.
|
||||
|
||||
|
@ -10,7 +25,7 @@ def retrieve_remote_profile(handle):
|
|||
|
||||
Currently, due to no other protocols supported, always use the Diaspora protocol.
|
||||
|
||||
:arg handle: The profile handle in format username@domain.tld
|
||||
:param handle: The profile handle in format username@domain.tld
|
||||
:returns: ``federation.entities.base.Profile`` or ``None``
|
||||
"""
|
||||
protocol_name = "diaspora"
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
from base64 import urlsafe_b64encode, b64encode, urlsafe_b64decode
|
||||
|
||||
from Crypto.Hash import SHA256
|
||||
from Crypto.Signature import PKCS1_v1_5 as PKCSSign
|
||||
from Crypto.PublicKey import RSA
|
||||
from Crypto.Signature import PKCS1_v1_5
|
||||
from lxml import etree
|
||||
|
||||
from federation.exceptions import SignatureVerificationError
|
||||
from federation.utils.diaspora import fetch_public_key
|
||||
from federation.utils.text import decode_if_bytes
|
||||
|
||||
NAMESPACE = "http://salmon-protocol.org/ns/magic-env"
|
||||
|
||||
|
@ -11,25 +15,70 @@ NAMESPACE = "http://salmon-protocol.org/ns/magic-env"
|
|||
class MagicEnvelope:
|
||||
"""Diaspora protocol magic envelope.
|
||||
|
||||
See: http://diaspora.github.io/diaspora_federation/federation/magicsig.html
|
||||
Can be used to construct and deconstruct MagicEnvelope documents.
|
||||
|
||||
When constructing, the following parameters should be given:
|
||||
* message
|
||||
* private_key
|
||||
* author_handle
|
||||
|
||||
When deconstructing, the following should be given:
|
||||
* payload
|
||||
* public_key (optional, will be fetched if not given, using either 'sender_key_fetcher' or remote server)
|
||||
|
||||
Upstream specification: http://diaspora.github.io/diaspora_federation/federation/magicsig.html
|
||||
"""
|
||||
|
||||
nsmap = {
|
||||
"me": NAMESPACE,
|
||||
}
|
||||
|
||||
def __init__(self, message, private_key, author_handle, wrap_payload=False):
|
||||
def __init__(self, message=None, private_key=None, author_handle=None, wrap_payload=False, payload=None,
|
||||
public_key=None, sender_key_fetcher=None, verify=False, doc=None):
|
||||
"""
|
||||
Args:
|
||||
wrap_payload (bool) - Whether to wrap the message in <XML><post></post></XML>.
|
||||
This is part of the legacy Diaspora protocol which will be removed in the future. (default False)
|
||||
All parameters are optional. Some are required for signing, some for opening.
|
||||
|
||||
:param message: Message string. Required to create a MagicEnvelope document.
|
||||
:param private_key: Private key RSA object.
|
||||
:param author_handle: Author signing the Magic Envelope, owns the private key.
|
||||
:param wrap_payload: - Boolean, whether to wrap the message in <XML><post></post></XML>.
|
||||
This is part of the legacy Diaspora protocol which will be removed in the future. (default False)
|
||||
:param payload: Magic Envelope payload as str or bytes.
|
||||
:param public_key: Author public key in str format.
|
||||
:param sender_key_fetcher: Function to use to fetch sender public key, if public key not given. Will fall back
|
||||
to network fetch of the profile and the key. Function must take handle as only parameter and return
|
||||
a public key string.
|
||||
:param verify: Verify after creating object, defaults to False.
|
||||
:param doc: MagicEnvelope document.
|
||||
"""
|
||||
self.message = message
|
||||
self._message = message
|
||||
self.private_key = private_key
|
||||
self.author_handle = author_handle
|
||||
self.wrap_payload = wrap_payload
|
||||
self.doc = None
|
||||
self.payload = None
|
||||
self.payload = payload
|
||||
self.public_key = public_key
|
||||
self.sender_key_fetcher = sender_key_fetcher
|
||||
if payload:
|
||||
self.extract_payload()
|
||||
elif doc is not None:
|
||||
self.doc = doc
|
||||
else:
|
||||
self.doc = None
|
||||
if verify:
|
||||
self.verify()
|
||||
|
||||
def extract_payload(self):
|
||||
payload = decode_if_bytes(self.payload)
|
||||
payload = payload.lstrip().encode("utf-8")
|
||||
self.doc = etree.fromstring(payload)
|
||||
self.author_handle = self.get_sender(self.doc)
|
||||
self.message = self.message_from_doc()
|
||||
|
||||
def fetch_public_key(self):
|
||||
if self.sender_key_fetcher:
|
||||
self.public_key = self.sender_key_fetcher(self.author_handle)
|
||||
return
|
||||
self.public_key = fetch_public_key(self.author_handle)
|
||||
|
||||
@staticmethod
|
||||
def get_sender(doc):
|
||||
|
@ -41,6 +90,19 @@ class MagicEnvelope:
|
|||
key_id = doc.find(".//{%s}sig" % NAMESPACE).get("key_id")
|
||||
return urlsafe_b64decode(key_id).decode("utf-8")
|
||||
|
||||
@property
|
||||
def message(self):
|
||||
return self._message
|
||||
|
||||
@message.setter
|
||||
def message(self, value):
|
||||
self._message = value
|
||||
|
||||
def message_from_doc(self):
|
||||
message = self.doc.find(
|
||||
".//{http://salmon-protocol.org/ns/magic-env}data").text
|
||||
return urlsafe_b64decode(message.encode("ascii"))
|
||||
|
||||
def create_payload(self):
|
||||
"""Create the payload doc.
|
||||
|
||||
|
@ -65,7 +127,7 @@ class MagicEnvelope:
|
|||
b64encode(b"base64url").decode("ascii") + "." + \
|
||||
b64encode(b"RSA-SHA256").decode("ascii")
|
||||
sig_hash = SHA256.new(sig_contents.encode("ascii"))
|
||||
cipher = PKCSSign.new(self.private_key)
|
||||
cipher = PKCS1_v1_5.new(self.private_key)
|
||||
sig = urlsafe_b64encode(cipher.sign(sig_hash))
|
||||
key_id = urlsafe_b64encode(bytes(self.author_handle, encoding="utf-8"))
|
||||
return sig, key_id
|
||||
|
@ -84,3 +146,20 @@ class MagicEnvelope:
|
|||
if self.doc is None:
|
||||
self.build()
|
||||
return etree.tostring(self.doc, encoding="unicode")
|
||||
|
||||
def verify(self):
|
||||
"""Verify Magic Envelope document against public key."""
|
||||
if not self.public_key:
|
||||
self.fetch_public_key()
|
||||
data = self.doc.find(".//{http://salmon-protocol.org/ns/magic-env}data").text
|
||||
sig = self.doc.find(".//{http://salmon-protocol.org/ns/magic-env}sig").text
|
||||
sig_contents = '.'.join([
|
||||
data,
|
||||
b64encode(b"application/xml").decode("ascii"),
|
||||
b64encode(b"base64url").decode("ascii"),
|
||||
b64encode(b"RSA-SHA256").decode("ascii")
|
||||
])
|
||||
sig_hash = SHA256.new(sig_contents.encode("ascii"))
|
||||
cipher = PKCS1_v1_5.new(RSA.importKey(self.public_key))
|
||||
if not cipher.verify(sig_hash, urlsafe_b64decode(sig)):
|
||||
raise SignatureVerificationError("Signature cannot be verified using the given public key")
|
||||
|
|
|
@ -5,15 +5,15 @@ from urllib.parse import unquote_plus
|
|||
|
||||
from Crypto.Cipher import AES, PKCS1_v1_5
|
||||
from Crypto.Hash import SHA256
|
||||
from Crypto.PublicKey import RSA
|
||||
from Crypto.Random import get_random_bytes
|
||||
from Crypto.Signature import PKCS1_v1_5 as PKCSSign
|
||||
from lxml import etree
|
||||
|
||||
from federation.exceptions import EncryptedMessageError, NoSenderKeyFoundError, SignatureVerificationError
|
||||
from federation.exceptions import EncryptedMessageError, NoSenderKeyFoundError
|
||||
from federation.protocols.base import BaseProtocol
|
||||
from federation.protocols.diaspora.encrypted import EncryptedPayload
|
||||
from federation.protocols.diaspora.magic_envelope import MagicEnvelope
|
||||
from federation.utils.diaspora import fetch_public_key
|
||||
from federation.utils.text import decode_if_bytes, encode_if_text
|
||||
|
||||
logger = logging.getLogger("federation")
|
||||
|
@ -174,23 +174,13 @@ class Protocol(BaseProtocol):
|
|||
Verify the signed XML elements to have confidence that the claimed
|
||||
author did actually generate this message.
|
||||
"""
|
||||
sender_key = self.get_contact_key(self.sender_handle)
|
||||
if self.get_contact_key:
|
||||
sender_key = self.get_contact_key(self.sender_handle)
|
||||
else:
|
||||
sender_key = fetch_public_key(self.sender_handle)
|
||||
if not sender_key:
|
||||
raise NoSenderKeyFoundError("Could not find a sender contact to retrieve key")
|
||||
body = self.doc.find(
|
||||
".//{http://salmon-protocol.org/ns/magic-env}data").text
|
||||
sig = self.doc.find(
|
||||
".//{http://salmon-protocol.org/ns/magic-env}sig").text
|
||||
sig_contents = '.'.join([
|
||||
body,
|
||||
b64encode(b"application/xml").decode("ascii"),
|
||||
b64encode(b"base64url").decode("ascii"),
|
||||
b64encode(b"RSA-SHA256").decode("ascii")
|
||||
])
|
||||
sig_hash = SHA256.new(sig_contents.encode("ascii"))
|
||||
cipher = PKCSSign.new(RSA.importKey(sender_key))
|
||||
if not cipher.verify(sig_hash, urlsafe_b64decode(sig)):
|
||||
raise SignatureVerificationError("Signature cannot be verified using the given contact key")
|
||||
MagicEnvelope(doc=self.doc, public_key=sender_key, verify=True)
|
||||
|
||||
def parse_header(self, b64data, key):
|
||||
"""
|
||||
|
|
|
@ -4,6 +4,7 @@ import pytest
|
|||
|
||||
from federation.entities.diaspora.entities import DiasporaPost
|
||||
from federation.tests.fixtures.keys import get_dummy_private_key
|
||||
from federation.tests.fixtures.payloads import DIASPORA_PUBLIC_PAYLOAD
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
|
@ -22,6 +23,11 @@ def disable_network_calls(monkeypatch):
|
|||
monkeypatch.setattr("requests.get", Mock(return_value=MockResponse))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def diaspora_public_payload():
|
||||
return DIASPORA_PUBLIC_PAYLOAD
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def diasporapost():
|
||||
return DiasporaPost()
|
||||
|
@ -30,3 +36,8 @@ def diasporapost():
|
|||
@pytest.fixture
|
||||
def private_key():
|
||||
return get_dummy_private_key()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def public_key(private_key):
|
||||
return private_key.publickey().exportKey()
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
from Crypto.PublicKey import RSA
|
||||
|
||||
|
||||
PRIVATE_KEY = "-----BEGIN RSA PRIVATE KEY-----\n" \
|
||||
"MIIEogIBAAKCAQEAiY2JBgMV90ULt0btku198l6wGuzn3xCcHs+eBZHL2C+XWRA3\n" \
|
||||
"BVDThSBj19dKXehfDphQ5u/Omfm76ImajEPHGBiYtZT7AgcO15zvm+JCpbREbdOV\n" \
|
||||
|
@ -29,6 +28,44 @@ PRIVATE_KEY = "-----BEGIN RSA PRIVATE KEY-----\n" \
|
|||
"w6Y5FnjFw022w+M3exyH6ZtxcmG6buDbp2F/SPD/FnYy5IFCDig=\n" \
|
||||
"-----END RSA PRIVATE KEY-----"
|
||||
|
||||
# Not related to above private key
|
||||
PUBKEY = "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAuCfU1G5X+3O6vPdSz6QY\nSFbgdbv3KPv" \
|
||||
"xHi8tRmlyOLdLt5i1eqsy2WCW1iYNijiCL7OfbrvymBQxe3GA9S64\nVuavwzQ8nO7nzpNMqxY5tBXsBM1lECCHDOvm5dzINXWT9Sg7P1" \
|
||||
"8iIxE/2wQEgMUL\nAeVbJtAriXM4zydL7c91agFMJu1aHp0lxzoH8I13xzUetGMutR1tbcfWvoQvPAoU\n89uAz5j/DFMhWrkVEKGeWt1" \
|
||||
"YtHMmJqpYqR6961GDlwRuUsOBsLgLLVohzlBsTBSn\n3580o2E6G3DEaX0Az9WB9ylhNeV/L/PP3c5htpEyoPZSy1pgtut6TRYQwC8wns" \
|
||||
"qO\nbVIbFBkrKoaRDyVCnpMuKdDNLZqOOfhzas+SWRAby6D8VsXpPi/DpeS9XkX0o/uH\nJ9N49GuYMSUGC8gKtaddD13pUqS/9rpSvLD" \
|
||||
"rrDQe5Lhuyusgd28wgEAPCTmM3pEt\nQnlxEeEmFMIn3OBLbEDw5TFE7iED0z7a4dAkqqz8KCGEt12e1Kz7ujuOVMxJxzk6\nNtwt40Sq" \
|
||||
"EOPcdsGHAA+hqzJnXUihXfmtmFkropaCxM2f+Ha0bOQdDDui5crcV3sX\njShmcqN6YqFzmoPK0XM9P1qC+lfL2Mz6bHC5p9M8/FtcM46" \
|
||||
"hCj1TF/tl8zaZxtHP\nOrMuFJy4j4yAsyVy3ddO69ECAwEAAQ==\n-----END PUBLIC KEY-----\n"
|
||||
|
||||
SIGNATURE = "A/vVRxM3V1ceEH1JrnPOaIZGM3gMjw/fnT9TgUh3poI4q9eH95AIoig+3eTA8XFuGvuo0tivxci4e0NJ1VLVkl/aqp8rvBNrRI1RQk" \
|
||||
"n2WVF6zk15Gq6KSia/wyzyiJHGxNGM8oFY4qPfNp6K+8ydUti22J11tVBEvQn+7FPAoloF2Xz1waK48ZZCFs8Rxzj+4jlz1PmuXCnT" \
|
||||
"j7v7GYS1Rb6sdFz4nBSuVk5X8tGOSXIRYxPgmtsDRMRrvDeEK+v3OY6VnT8dLTckS0qCwTRUULub1CGwkz/2mReZk/M1W4EbUnugF5" \
|
||||
"ptslmFqYDYJZM8PA/g89EKVpkx2gaFbsC4KXocWnxHNiue18rrFQ5hMnDuDRiRybLnQkxXbE/HDuLdnognt2S5wRshPoZmhe95v3qq" \
|
||||
"/5nH/GX1D7VmxEEIG9fX+XX+Vh9kzO9bLbwoJZwm50zXxCvrLlye/2JU5Vd2Hbm4aMuAyRAZiLS/EQcBlsts4DaFu4txe60HbXSh6n" \
|
||||
"qNofGkusuzZnCd0VObOpXizrI8xNQzZpjJEB5QqE2gbCC2YZNdOS0eBGXw42dAXa/QV3jZXGES7DdQlqPqqT3YjcMFLiRrWQR8cl4h" \
|
||||
"JIBRpV5piGyLmMMKYrWu7hQSrdRAEL3K6mNZZU6/yoG879LjtQbVwaFGPeT29B4zBE97FIo="
|
||||
|
||||
SIGNATURE2 = "Xla/AlirMihx72hehGMgpKILRUA2ZkEhFgVc65sl80iN+F62yQdSikGyUQVL+LaGNUgmzgK0zEahamfaMFep/9HE2FWuXlTCM+ZXx" \
|
||||
"OhGWUnjkGW9vi41/Turm7ALzaJoFm1f3Iv4nh1sRD1jySzlZvYwrq4LwmgZ8r0M+Q6xUSIIJfgS8Zjmp43strKo28vKT+DmUKu9Fg" \
|
||||
"jZWjW3S8WPPJFO0UqA0b1UQspmNLZOVxsNpa0OCM1pofJvT09n6xG+byV30Bed27Kw+D3fzfYq5xvohyeCyliTq8LHnOykecki3Y2" \
|
||||
"Pvl1qsxxBehlwc/WH8yIUiwC2Du6zY61tN3LGgMAoIFl40Roo1z/I7YfOy4ZCukOGqqyiLdjoXxIVQqqsPtKsrVXS+A9OQ+sVESgw" \
|
||||
"f8jeEIw/KXLVB/aEyrZJXQR1pBfqkOTCSnAfZVBSjJyxhanS/8iGmnRV5zz3auYMLR9aA8QHjV/VZOj0Bxhuba9VIzJlY9XoUt5Vs" \
|
||||
"h3uILJM3uVJzSjlZV+Jw3O+NdQFnZyh7m1+eJUMQJ8i0Sr3sMLsdb9me/I0HueXCa5eBHAoTtAyQgS4uN4NMhvpqrB/lQCx7pqnkt" \
|
||||
"xiCO/bUEZONQjWrvJT+EfD+I0UMFtPFiGDzJ0yi0Ah7LxSTGEGPFZHH5RgsJA8lJwGMCUtc9Cpy8A="
|
||||
|
||||
SIGNATURE3 = "hVdLwsWXe6yVy88m9H1903+Bj/DjSGsYL+ZIpEz+G6u/aVx6QfsvnWHzasjqN8SU+brHfL0c8KrapWcACO+jyCuXlHMZb9zKmJkHR" \
|
||||
"FSOiprCJ3tqNpv/4MIa9CXu0YDqnLHBSyxS01luKw3EqgpWPQdYcqDpOkjjTOq45dQC0PGHA/DXjP7LBptV9AwW200LIcL5Li8tDU" \
|
||||
"a8VSQybspDDfDpXU3+Xl5tJIBVS4ercPczp5B39Cwne4q2gyj/Y5RdIoX5RMqmFhfucw1he38T1oRC9AHTJqj4CBcDt7gc6jPHuzk" \
|
||||
"N7u1eUf0IK3+KTDKsCkkoHcGaoxT+NeWcS8Ki1A=="
|
||||
|
||||
XML = "<comment><guid>0dd40d800db1013514416c626dd55703</guid><parent_guid>69ab2b83-aa69-4456-ad0a-dd669" \
|
||||
"7f54714</parent_guid><text>Woop Woop</text><diaspora_handle>jaywink@iliketoast.net</diaspora_handle></comment>"
|
||||
|
||||
XML2 = "<comment><guid>d728fe501584013514526c626dd55703</guid><parent_guid>d641bd35-8142-414e-a12d-f956cc2c1bb9" \
|
||||
"</parent_guid><text>What about the mystical problem with 👍 (pt2 with more logging)</text>" \
|
||||
"<diaspora_handle>jaywink@iliketoast.net</diaspora_handle></comment>"
|
||||
|
||||
|
||||
def get_dummy_private_key():
|
||||
return RSA.importKey(PRIVATE_KEY)
|
||||
|
|
|
@ -1,21 +1,16 @@
|
|||
from lxml import etree
|
||||
from unittest.mock import patch, Mock
|
||||
|
||||
from Crypto import Random
|
||||
from Crypto.PublicKey import RSA
|
||||
import pytest
|
||||
from lxml import etree
|
||||
from lxml.etree import _Element
|
||||
|
||||
from federation.exceptions import SignatureVerificationError
|
||||
from federation.protocols.diaspora.magic_envelope import MagicEnvelope
|
||||
from federation.tests.fixtures.keys import get_dummy_private_key
|
||||
from federation.tests.fixtures.keys import get_dummy_private_key, PUBKEY
|
||||
from federation.tests.fixtures.payloads import DIASPORA_PUBLIC_PAYLOAD
|
||||
|
||||
|
||||
class TestMagicEnvelope():
|
||||
@staticmethod
|
||||
def generate_rsa_private_key():
|
||||
"""Generate a new RSA private key."""
|
||||
rand = Random.new().read
|
||||
return RSA.generate(2048, rand)
|
||||
|
||||
class TestMagicEnvelope:
|
||||
def test_build(self):
|
||||
env = MagicEnvelope(
|
||||
message="<status_message><foo>bar</foo></status_message>",
|
||||
|
@ -40,11 +35,74 @@ class TestMagicEnvelope():
|
|||
env = MagicEnvelope(
|
||||
message="<status_message><foo>bar</foo></status_message>",
|
||||
private_key="key",
|
||||
author_handle="foobar@example.com"
|
||||
author_handle="foobar@example.com",
|
||||
)
|
||||
payload = env.create_payload()
|
||||
assert payload == "PHN0YXR1c19tZXNzYWdlPjxmb28-YmFyPC9mb28-PC9zdGF0dXNfbWVzc2FnZT4="
|
||||
|
||||
def test_extract_payload(self, diaspora_public_payload):
|
||||
env = MagicEnvelope()
|
||||
env.payload = diaspora_public_payload
|
||||
assert not env.doc
|
||||
assert not env.author_handle
|
||||
assert not env.message
|
||||
env.extract_payload()
|
||||
assert isinstance(env.doc, _Element)
|
||||
assert env.author_handle == "foobar@example.com"
|
||||
assert env.message == b"<status_message><foo>bar</foo></status_message>"
|
||||
|
||||
@patch("federation.protocols.diaspora.magic_envelope.fetch_public_key", autospec=True)
|
||||
def test_fetch_public_key__calls_sender_key_fetcher(self, mock_fetch):
|
||||
mock_fetcher = Mock(return_value="public key")
|
||||
env = MagicEnvelope(author_handle="spam@eggs", sender_key_fetcher=mock_fetcher)
|
||||
env.fetch_public_key()
|
||||
mock_fetcher.assert_called_once_with("spam@eggs")
|
||||
assert not mock_fetch.called
|
||||
|
||||
@patch("federation.protocols.diaspora.magic_envelope.fetch_public_key", autospec=True)
|
||||
def test_fetch_public_key__calls_fetch_public_key(self, mock_fetch):
|
||||
env = MagicEnvelope(author_handle="spam@eggs")
|
||||
env.fetch_public_key()
|
||||
mock_fetch.assert_called_once_with("spam@eggs")
|
||||
|
||||
def test_message_from_doc(self, diaspora_public_payload):
|
||||
env = MagicEnvelope(payload=diaspora_public_payload)
|
||||
assert env.message_from_doc() == env.message
|
||||
|
||||
def test_payload_extracted_on_init(self, diaspora_public_payload):
|
||||
env = MagicEnvelope(payload=diaspora_public_payload)
|
||||
assert isinstance(env.doc, _Element)
|
||||
assert env.author_handle == "foobar@example.com"
|
||||
assert env.message == b"<status_message><foo>bar</foo></status_message>"
|
||||
|
||||
def test_verify(self, private_key, public_key):
|
||||
me = MagicEnvelope(
|
||||
message="<status_message><foo>bar</foo></status_message>",
|
||||
private_key=private_key,
|
||||
author_handle="foobar@example.com"
|
||||
)
|
||||
me.build()
|
||||
output = me.render()
|
||||
|
||||
MagicEnvelope(payload=output, public_key=public_key, verify=True)
|
||||
|
||||
with pytest.raises(SignatureVerificationError):
|
||||
MagicEnvelope(payload=output, public_key=PUBKEY, verify=True)
|
||||
|
||||
def test_verify__calls_fetch_public_key(self, diaspora_public_payload):
|
||||
me = MagicEnvelope(payload=diaspora_public_payload)
|
||||
with pytest.raises(TypeError):
|
||||
with patch.object(me, "fetch_public_key") as mock_fetch:
|
||||
me.verify()
|
||||
mock_fetch.assert_called_once_with()
|
||||
|
||||
@patch("federation.protocols.diaspora.magic_envelope.MagicEnvelope.verify")
|
||||
def test_verify_on_init(self, mock_verify, diaspora_public_payload):
|
||||
MagicEnvelope(payload=diaspora_public_payload)
|
||||
assert not mock_verify.called
|
||||
MagicEnvelope(payload=diaspora_public_payload, verify=True)
|
||||
assert mock_verify.called
|
||||
|
||||
def test_build_signature(self):
|
||||
env = MagicEnvelope(
|
||||
message="<status_message><foo>bar</foo></status_message>",
|
||||
|
|
|
@ -5,16 +5,16 @@ from xml.etree.ElementTree import ElementTree
|
|||
from lxml import etree
|
||||
import pytest
|
||||
|
||||
from federation.exceptions import EncryptedMessageError, NoSenderKeyFoundError
|
||||
from federation.exceptions import EncryptedMessageError, NoSenderKeyFoundError, SignatureVerificationError
|
||||
from federation.protocols.diaspora.protocol import Protocol, identify_payload
|
||||
from federation.tests.fixtures.keys import PUBKEY
|
||||
from federation.tests.fixtures.payloads import (
|
||||
ENCRYPTED_LEGACY_DIASPORA_PAYLOAD, UNENCRYPTED_LEGACY_DIASPORA_PAYLOAD,
|
||||
DIASPORA_PUBLIC_PAYLOAD,
|
||||
ENCRYPTED_LEGACY_DIASPORA_PAYLOAD, UNENCRYPTED_LEGACY_DIASPORA_PAYLOAD, DIASPORA_PUBLIC_PAYLOAD,
|
||||
DIASPORA_ENCRYPTED_PAYLOAD,
|
||||
)
|
||||
|
||||
|
||||
class MockUser():
|
||||
class MockUser:
|
||||
private_key = "foobar"
|
||||
|
||||
def __init__(self, nokey=False):
|
||||
|
@ -30,7 +30,7 @@ def mock_not_found_get_contact_key(contact):
|
|||
return None
|
||||
|
||||
|
||||
class DiasporaTestBase():
|
||||
class DiasporaTestBase:
|
||||
def init_protocol(self):
|
||||
return Protocol()
|
||||
|
||||
|
@ -99,11 +99,37 @@ class TestDiasporaProtocol(DiasporaTestBase):
|
|||
with pytest.raises(EncryptedMessageError):
|
||||
protocol.receive(ENCRYPTED_LEGACY_DIASPORA_PAYLOAD, user)
|
||||
|
||||
def test_receive_raises_if_sender_key_cannot_be_found(self):
|
||||
@patch("federation.protocols.diaspora.protocol.fetch_public_key", autospec=True)
|
||||
def test_receive_raises_if_sender_key_cannot_be_found(self, mock_fetch):
|
||||
protocol = self.init_protocol()
|
||||
user = self.get_mock_user()
|
||||
with pytest.raises(NoSenderKeyFoundError):
|
||||
protocol.receive(UNENCRYPTED_LEGACY_DIASPORA_PAYLOAD, user, mock_not_found_get_contact_key)
|
||||
assert not mock_fetch.called
|
||||
|
||||
@patch("federation.protocols.diaspora.protocol.fetch_public_key", autospec=True, return_value=None)
|
||||
def test_receive_calls_fetch_public_key_if_key_fetcher_not_given(self, mock_fetch):
|
||||
protocol = self.init_protocol()
|
||||
user = self.get_mock_user()
|
||||
with pytest.raises(NoSenderKeyFoundError):
|
||||
protocol.receive(UNENCRYPTED_LEGACY_DIASPORA_PAYLOAD, user)
|
||||
mock_fetch.assert_called_once_with("bob@example.com")
|
||||
|
||||
@patch("federation.protocols.diaspora.protocol.MagicEnvelope", autospec=True)
|
||||
@patch("federation.protocols.diaspora.protocol.fetch_public_key", autospec=True, return_value="key")
|
||||
def test_receive_creates_and_verifies_magic_envelope_instance(self, mock_fetch, mock_env):
|
||||
protocol = self.init_protocol()
|
||||
user = self.get_mock_user()
|
||||
protocol.receive(UNENCRYPTED_LEGACY_DIASPORA_PAYLOAD, user)
|
||||
mock_env.assert_called_once_with(doc=protocol.doc, public_key="key", verify=True)
|
||||
|
||||
@patch("federation.protocols.diaspora.protocol.fetch_public_key", autospec=True)
|
||||
def test_receive_raises_on_signature_verification_failure(self, mock_fetch):
|
||||
mock_fetch.return_value = PUBKEY
|
||||
protocol = self.init_protocol()
|
||||
user = self.get_mock_user()
|
||||
with pytest.raises(SignatureVerificationError):
|
||||
protocol.receive(DIASPORA_PUBLIC_PAYLOAD, user)
|
||||
|
||||
def test_get_message_content(self):
|
||||
protocol = self.init_protocol()
|
||||
|
|
|
@ -1,44 +1,7 @@
|
|||
from lxml import etree
|
||||
|
||||
from federation.protocols.diaspora.signatures import verify_relayable_signature, create_relayable_signature
|
||||
from federation.tests.fixtures.keys import get_dummy_private_key
|
||||
|
||||
XML = "<comment><guid>0dd40d800db1013514416c626dd55703</guid><parent_guid>69ab2b83-aa69-4456-ad0a-dd669" \
|
||||
"7f54714</parent_guid><text>Woop Woop</text><diaspora_handle>jaywink@iliketoast.net</diaspora_handle></comment>"
|
||||
|
||||
XML2 = "<comment><guid>d728fe501584013514526c626dd55703</guid><parent_guid>d641bd35-8142-414e-a12d-f956cc2c1bb9" \
|
||||
"</parent_guid><text>What about the mystical problem with 👍 (pt2 with more logging)</text>" \
|
||||
"<diaspora_handle>jaywink@iliketoast.net</diaspora_handle></comment>"
|
||||
|
||||
SIGNATURE = "A/vVRxM3V1ceEH1JrnPOaIZGM3gMjw/fnT9TgUh3poI4q9eH95AIoig+3eTA8XFuGvuo0tivxci4e0NJ1VLVkl/aqp8rvBNrRI1RQk" \
|
||||
"n2WVF6zk15Gq6KSia/wyzyiJHGxNGM8oFY4qPfNp6K+8ydUti22J11tVBEvQn+7FPAoloF2Xz1waK48ZZCFs8Rxzj+4jlz1PmuXCnT" \
|
||||
"j7v7GYS1Rb6sdFz4nBSuVk5X8tGOSXIRYxPgmtsDRMRrvDeEK+v3OY6VnT8dLTckS0qCwTRUULub1CGwkz/2mReZk/M1W4EbUnugF5" \
|
||||
"ptslmFqYDYJZM8PA/g89EKVpkx2gaFbsC4KXocWnxHNiue18rrFQ5hMnDuDRiRybLnQkxXbE/HDuLdnognt2S5wRshPoZmhe95v3qq" \
|
||||
"/5nH/GX1D7VmxEEIG9fX+XX+Vh9kzO9bLbwoJZwm50zXxCvrLlye/2JU5Vd2Hbm4aMuAyRAZiLS/EQcBlsts4DaFu4txe60HbXSh6n" \
|
||||
"qNofGkusuzZnCd0VObOpXizrI8xNQzZpjJEB5QqE2gbCC2YZNdOS0eBGXw42dAXa/QV3jZXGES7DdQlqPqqT3YjcMFLiRrWQR8cl4h" \
|
||||
"JIBRpV5piGyLmMMKYrWu7hQSrdRAEL3K6mNZZU6/yoG879LjtQbVwaFGPeT29B4zBE97FIo="
|
||||
|
||||
SIGNATURE2 = "Xla/AlirMihx72hehGMgpKILRUA2ZkEhFgVc65sl80iN+F62yQdSikGyUQVL+LaGNUgmzgK0zEahamfaMFep/9HE2FWuXlTCM+ZXx" \
|
||||
"OhGWUnjkGW9vi41/Turm7ALzaJoFm1f3Iv4nh1sRD1jySzlZvYwrq4LwmgZ8r0M+Q6xUSIIJfgS8Zjmp43strKo28vKT+DmUKu9Fg" \
|
||||
"jZWjW3S8WPPJFO0UqA0b1UQspmNLZOVxsNpa0OCM1pofJvT09n6xG+byV30Bed27Kw+D3fzfYq5xvohyeCyliTq8LHnOykecki3Y2" \
|
||||
"Pvl1qsxxBehlwc/WH8yIUiwC2Du6zY61tN3LGgMAoIFl40Roo1z/I7YfOy4ZCukOGqqyiLdjoXxIVQqqsPtKsrVXS+A9OQ+sVESgw" \
|
||||
"f8jeEIw/KXLVB/aEyrZJXQR1pBfqkOTCSnAfZVBSjJyxhanS/8iGmnRV5zz3auYMLR9aA8QHjV/VZOj0Bxhuba9VIzJlY9XoUt5Vs" \
|
||||
"h3uILJM3uVJzSjlZV+Jw3O+NdQFnZyh7m1+eJUMQJ8i0Sr3sMLsdb9me/I0HueXCa5eBHAoTtAyQgS4uN4NMhvpqrB/lQCx7pqnkt" \
|
||||
"xiCO/bUEZONQjWrvJT+EfD+I0UMFtPFiGDzJ0yi0Ah7LxSTGEGPFZHH5RgsJA8lJwGMCUtc9Cpy8A="
|
||||
|
||||
SIGNATURE3 = "hVdLwsWXe6yVy88m9H1903+Bj/DjSGsYL+ZIpEz+G6u/aVx6QfsvnWHzasjqN8SU+brHfL0c8KrapWcACO+jyCuXlHMZb9zKmJkHR" \
|
||||
"FSOiprCJ3tqNpv/4MIa9CXu0YDqnLHBSyxS01luKw3EqgpWPQdYcqDpOkjjTOq45dQC0PGHA/DXjP7LBptV9AwW200LIcL5Li8tDU" \
|
||||
"a8VSQybspDDfDpXU3+Xl5tJIBVS4ercPczp5B39Cwne4q2gyj/Y5RdIoX5RMqmFhfucw1he38T1oRC9AHTJqj4CBcDt7gc6jPHuzk" \
|
||||
"N7u1eUf0IK3+KTDKsCkkoHcGaoxT+NeWcS8Ki1A=="
|
||||
|
||||
PUBKEY = "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAuCfU1G5X+3O6vPdSz6QY\nSFbgdbv3KPv" \
|
||||
"xHi8tRmlyOLdLt5i1eqsy2WCW1iYNijiCL7OfbrvymBQxe3GA9S64\nVuavwzQ8nO7nzpNMqxY5tBXsBM1lECCHDOvm5dzINXWT9Sg7P1" \
|
||||
"8iIxE/2wQEgMUL\nAeVbJtAriXM4zydL7c91agFMJu1aHp0lxzoH8I13xzUetGMutR1tbcfWvoQvPAoU\n89uAz5j/DFMhWrkVEKGeWt1" \
|
||||
"YtHMmJqpYqR6961GDlwRuUsOBsLgLLVohzlBsTBSn\n3580o2E6G3DEaX0Az9WB9ylhNeV/L/PP3c5htpEyoPZSy1pgtut6TRYQwC8wns" \
|
||||
"qO\nbVIbFBkrKoaRDyVCnpMuKdDNLZqOOfhzas+SWRAby6D8VsXpPi/DpeS9XkX0o/uH\nJ9N49GuYMSUGC8gKtaddD13pUqS/9rpSvLD" \
|
||||
"rrDQe5Lhuyusgd28wgEAPCTmM3pEt\nQnlxEeEmFMIn3OBLbEDw5TFE7iED0z7a4dAkqqz8KCGEt12e1Kz7ujuOVMxJxzk6\nNtwt40Sq" \
|
||||
"EOPcdsGHAA+hqzJnXUihXfmtmFkropaCxM2f+Ha0bOQdDDui5crcV3sX\njShmcqN6YqFzmoPK0XM9P1qC+lfL2Mz6bHC5p9M8/FtcM46" \
|
||||
"hCj1TF/tl8zaZxtHP\nOrMuFJy4j4yAsyVy3ddO69ECAwEAAQ==\n-----END PUBLIC KEY-----\n"
|
||||
from federation.protocols.diaspora.signatures import create_relayable_signature, verify_relayable_signature
|
||||
from federation.tests.fixtures.keys import PUBKEY, SIGNATURE, SIGNATURE2, SIGNATURE3, XML, XML2, get_dummy_private_key
|
||||
|
||||
|
||||
def test_verify_relayable_signature():
|
||||
|
|
|
@ -1,16 +1,24 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from unittest.mock import patch, Mock
|
||||
|
||||
from federation.fetchers import retrieve_remote_profile
|
||||
from federation.entities.base import Post
|
||||
from federation.fetchers import retrieve_remote_profile, retrieve_remote_content
|
||||
|
||||
|
||||
class TestRetrieveRemoteProfile(object):
|
||||
class TestRetrieveRemoteContent:
|
||||
@patch("federation.fetchers.importlib.import_module")
|
||||
def test_calls_diaspora_retrieve_and_parse_content(self, mock_import):
|
||||
mock_retrieve = Mock()
|
||||
mock_import.return_value = mock_retrieve
|
||||
retrieve_remote_content(Post, "1234@example.com", sender_key_fetcher=sum)
|
||||
mock_retrieve.retrieve_and_parse_content.assert_called_once_with(
|
||||
Post, "1234@example.com", sender_key_fetcher=sum,
|
||||
)
|
||||
|
||||
|
||||
class TestRetrieveRemoteProfile:
|
||||
@patch("federation.fetchers.importlib.import_module")
|
||||
def test_calls_diaspora_retrieve_and_parse_profile(self, mock_import):
|
||||
class MockRetrieve(Mock):
|
||||
def retrieve_and_parse_profile(self, handle):
|
||||
return "called with %s" % handle
|
||||
|
||||
mock_retrieve = MockRetrieve()
|
||||
mock_retrieve = Mock()
|
||||
mock_import.return_value = mock_retrieve
|
||||
assert retrieve_remote_profile("foo@bar") == "called with foo@bar"
|
||||
retrieve_remote_profile("foo@bar")
|
||||
mock_retrieve.retrieve_and_parse_profile.assert_called_once_with("foo@bar")
|
||||
|
|
|
@ -1,17 +1,33 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import xml
|
||||
from unittest.mock import patch, Mock
|
||||
from urllib.parse import quote
|
||||
|
||||
import pytest
|
||||
from lxml import html
|
||||
|
||||
from federation.entities.base import Profile
|
||||
from federation.hostmeta.generators import DiasporaWebFinger, DiasporaHostMeta, DiasporaHCard, generate_hcard
|
||||
from federation.utils.diaspora import retrieve_diaspora_hcard, retrieve_diaspora_webfinger, retrieve_diaspora_host_meta, \
|
||||
_get_element_text_or_none, _get_element_attr_or_none, parse_profile_from_hcard, retrieve_and_parse_profile
|
||||
from federation.entities.base import Profile, Post
|
||||
from federation.hostmeta.generators import DiasporaWebFinger, DiasporaHostMeta, generate_hcard
|
||||
from federation.tests.fixtures.payloads import DIASPORA_PUBLIC_PAYLOAD
|
||||
from federation.utils.diaspora import (
|
||||
retrieve_diaspora_hcard, retrieve_diaspora_webfinger, retrieve_diaspora_host_meta, _get_element_text_or_none,
|
||||
_get_element_attr_or_none, parse_profile_from_hcard, retrieve_and_parse_profile, retrieve_and_parse_content,
|
||||
get_fetch_content_endpoint, fetch_public_key)
|
||||
|
||||
|
||||
class TestRetrieveDiasporaHCard(object):
|
||||
@patch("federation.utils.diaspora.retrieve_and_parse_profile", autospec=True)
|
||||
def test_fetch_public_key(mock_retrieve):
|
||||
mock_retrieve.return_value = Mock(public_key="public key")
|
||||
result = fetch_public_key("spam@eggs")
|
||||
mock_retrieve.assert_called_once_with("spam@eggs")
|
||||
assert result == "public key"
|
||||
|
||||
|
||||
def test_get_fetch_content_endpoint():
|
||||
assert get_fetch_content_endpoint("example.com", "status_message", "1234") == \
|
||||
"https://example.com/fetch/status_message/1234"
|
||||
|
||||
|
||||
class TestRetrieveDiasporaHCard:
|
||||
@patch("federation.utils.diaspora.retrieve_diaspora_webfinger", return_value=None)
|
||||
def test_retrieve_webfinger_is_called(self, mock_retrieve):
|
||||
retrieve_diaspora_hcard("bob@localhost")
|
||||
|
@ -40,7 +56,7 @@ class TestRetrieveDiasporaHCard(object):
|
|||
assert document == None
|
||||
|
||||
|
||||
class TestRetrieveDiasporaWebfinger(object):
|
||||
class TestRetrieveDiasporaWebfinger:
|
||||
@patch("federation.utils.diaspora.retrieve_diaspora_host_meta", return_value=None)
|
||||
def test_retrieve_host_meta_is_called(self, mock_retrieve):
|
||||
retrieve_diaspora_webfinger("bob@localhost")
|
||||
|
@ -82,7 +98,7 @@ class TestRetrieveDiasporaWebfinger(object):
|
|||
assert document == None
|
||||
|
||||
|
||||
class TestRetrieveDiasporaHostMeta(object):
|
||||
class TestRetrieveDiasporaHostMeta:
|
||||
@patch("federation.utils.diaspora.XRD.parse_xrd")
|
||||
@patch("federation.utils.diaspora.fetch_document")
|
||||
def test_fetch_document_is_called(self, mock_fetch, mock_xrd):
|
||||
|
@ -100,7 +116,48 @@ class TestRetrieveDiasporaHostMeta(object):
|
|||
assert document == None
|
||||
|
||||
|
||||
class TestGetElementTextOrNone(object):
|
||||
class TestRetrieveAndParseContent:
|
||||
@patch("federation.utils.diaspora.fetch_document", return_value=(None, 404, None))
|
||||
@patch("federation.utils.diaspora.get_fetch_content_endpoint", return_value="https://example.com/fetch/spam/eggs")
|
||||
def test_calls_fetch_document(self, mock_get, mock_fetch):
|
||||
retrieve_and_parse_content(Post, "1234@example.com")
|
||||
mock_fetch.assert_called_once_with("https://example.com/fetch/spam/eggs")
|
||||
|
||||
@patch("federation.utils.diaspora.fetch_document", return_value=(None, 404, None))
|
||||
@patch("federation.utils.diaspora.get_fetch_content_endpoint")
|
||||
def test_calls_get_fetch_content_endpoint(self, mock_get, mock_fetch):
|
||||
retrieve_and_parse_content(Post, "1234@example.com")
|
||||
mock_get.assert_called_once_with("example.com", "status_message", "1234")
|
||||
mock_get.reset_mock()
|
||||
retrieve_and_parse_content(Post, "fooobar@1234@example.com")
|
||||
mock_get.assert_called_once_with("example.com", "status_message", "fooobar@1234")
|
||||
|
||||
@patch("federation.utils.diaspora.fetch_document", return_value=(DIASPORA_PUBLIC_PAYLOAD, 200, None))
|
||||
@patch("federation.utils.diaspora.get_fetch_content_endpoint", return_value="https://example.com/fetch/spam/eggs")
|
||||
@patch("federation.utils.diaspora.handle_receive", return_value=("sender", "protocol", ["entity"]))
|
||||
def test_calls_handle_receive(self, mock_handle, mock_get, mock_fetch):
|
||||
entity = retrieve_and_parse_content(Post, "1234@example.com", sender_key_fetcher=sum)
|
||||
mock_handle.assert_called_once_with(DIASPORA_PUBLIC_PAYLOAD, sender_key_fetcher=sum)
|
||||
assert entity == "entity"
|
||||
|
||||
@patch("federation.utils.diaspora.fetch_document", return_value=(None, None, Exception()))
|
||||
@patch("federation.utils.diaspora.get_fetch_content_endpoint", return_value="https://example.com/fetch/spam/eggs")
|
||||
def test_raises_on_fetch_error(self, mock_get, mock_fetch):
|
||||
with pytest.raises(Exception):
|
||||
retrieve_and_parse_content(Post, "1234@example.com")
|
||||
|
||||
def test_raises_on_unknown_entity(self):
|
||||
with pytest.raises(ValueError):
|
||||
retrieve_and_parse_content(dict, "1234@example.com")
|
||||
|
||||
@patch("federation.utils.diaspora.fetch_document", return_value=(None, 404, None))
|
||||
@patch("federation.utils.diaspora.get_fetch_content_endpoint", return_value="https://example.com/fetch/spam/eggs")
|
||||
def test_returns_on_404(self, mock_get, mock_fetch):
|
||||
result = retrieve_and_parse_content(Post, "1234@example.com")
|
||||
assert not result
|
||||
|
||||
|
||||
class TestGetElementTextOrNone:
|
||||
doc = html.fromstring("<foo>bar</foo>")
|
||||
|
||||
def test_text_returned_on_element(self):
|
||||
|
@ -110,7 +167,7 @@ class TestGetElementTextOrNone(object):
|
|||
assert _get_element_text_or_none(self.doc, "bar") == None
|
||||
|
||||
|
||||
class TestGetElementAttrOrNone(object):
|
||||
class TestGetElementAttrOrNone:
|
||||
doc = html.fromstring("<foo src='baz'>bar</foo>")
|
||||
|
||||
def test_attr_returned_on_attr(self):
|
||||
|
@ -123,7 +180,7 @@ class TestGetElementAttrOrNone(object):
|
|||
assert _get_element_attr_or_none(self.doc, "bar", "href") == None
|
||||
|
||||
|
||||
class TestParseProfileFromHCard(object):
|
||||
class TestParseProfileFromHCard:
|
||||
def test_profile_is_parsed(self):
|
||||
hcard = generate_hcard(
|
||||
"diaspora",
|
||||
|
@ -151,7 +208,7 @@ class TestParseProfileFromHCard(object):
|
|||
profile.validate()
|
||||
|
||||
|
||||
class TestRetrieveAndParseProfile(object):
|
||||
class TestRetrieveAndParseProfile:
|
||||
@patch("federation.utils.diaspora.retrieve_diaspora_hcard", return_value=None)
|
||||
def test_retrieve_diaspora_hcard_is_called(self, mock_retrieve):
|
||||
retrieve_and_parse_profile("foo@bar")
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import logging
|
||||
import xml
|
||||
from urllib.parse import quote
|
||||
|
@ -7,11 +6,22 @@ from lxml import html
|
|||
from xrd import XRD
|
||||
|
||||
from federation.entities.base import Profile
|
||||
from federation.inbound import handle_receive
|
||||
from federation.utils.network import fetch_document
|
||||
|
||||
logger = logging.getLogger("federation")
|
||||
|
||||
|
||||
def fetch_public_key(handle):
|
||||
"""Fetch public key over the network.
|
||||
|
||||
:param handle: Remote handle to retrieve public key for.
|
||||
:return: Public key in str format from parsed profile.
|
||||
"""
|
||||
profile = retrieve_and_parse_profile(handle)
|
||||
return profile.public_key
|
||||
|
||||
|
||||
def retrieve_diaspora_hcard(handle):
|
||||
"""
|
||||
Retrieve a remote Diaspora hCard document.
|
||||
|
@ -122,6 +132,43 @@ def parse_profile_from_hcard(hcard, handle):
|
|||
return profile
|
||||
|
||||
|
||||
def retrieve_and_parse_content(entity_class, id, sender_key_fetcher=None):
|
||||
"""Retrieve remote content and return an Entity class instance.
|
||||
|
||||
This is basically the inverse of receiving an entity. Instead, we fetch it, then call 'handle_receive'.
|
||||
|
||||
:param entity_class: Federation entity class (from ``federation.entity.base``).
|
||||
:param id: GUID and domain of the remote entity, in format``guid@domain.tld``.
|
||||
:param sender_key_fetcher: Function to use to fetch sender public key. If not given, network will be used
|
||||
to fetch the profile and the key. Function must take handle as only parameter and return a public key.
|
||||
:returns: Entity object instance or ``None``
|
||||
:raises: ``ValueError`` if ``entity_class`` is not valid.
|
||||
"""
|
||||
from federation.entities.diaspora.mappers import BASE_MAPPINGS
|
||||
entity_type = BASE_MAPPINGS.get(entity_class)
|
||||
if not entity_type:
|
||||
raise ValueError("Unknown entity_class %s" % entity_class)
|
||||
guid, domain = id.rsplit("@", 1)
|
||||
url = get_fetch_content_endpoint(domain, entity_type, guid)
|
||||
document, status_code, error = fetch_document(url)
|
||||
if status_code == 200:
|
||||
_sender, _protocol, entities = handle_receive(document, sender_key_fetcher=sender_key_fetcher)
|
||||
if len(entities) > 1:
|
||||
logger.warning("retrieve_and_parse_content - more than one entity parsed from remote even though we"
|
||||
"expected only one! ID %s", id)
|
||||
if entities:
|
||||
return entities[0]
|
||||
return
|
||||
elif status_code == 404:
|
||||
logger.warning("retrieve_and_parse_content - remote content %s not found", id)
|
||||
return
|
||||
if error:
|
||||
raise error
|
||||
raise Exception("retrieve_and_parse_content - unknown problem when fetching document: %s, %s, %s" % (
|
||||
document, status_code, error,
|
||||
))
|
||||
|
||||
|
||||
def retrieve_and_parse_profile(handle):
|
||||
"""
|
||||
Retrieve the remote user and return a Profile object.
|
||||
|
@ -142,5 +189,14 @@ def retrieve_and_parse_profile(handle):
|
|||
return profile
|
||||
|
||||
|
||||
def get_fetch_content_endpoint(domain, entity_type, guid):
|
||||
"""Get remote fetch content endpoint.
|
||||
|
||||
See: https://diaspora.github.io/diaspora_federation/federation/fetching.html
|
||||
"""
|
||||
return "https://%s/fetch/%s/%s" % (domain, entity_type, guid)
|
||||
|
||||
|
||||
def get_public_endpoint(domain):
|
||||
"""Get remote endpoint for delivering public payloads."""
|
||||
return "https://%s/receive/public" % domain
|
||||
|
|
Ładowanie…
Reference in New Issue