kopia lustrzana https://gitlab.com/jaywink/federation
Add RFC3033 webfinger generator
Also provided is a Django view and url configuration for easy addition into Django projects. Django is not a hard dependency of this library, usage of the Django view obviously requires installing Django itself. For configuration details see documentation. Closes #108merge-requests/130/head
rodzic
456d8344d8
commit
f80211b178
|
@ -7,6 +7,10 @@
|
|||
* Enable generating encrypted JSON payloads with the Diaspora protocol which adds private message support. ([related issue](https://github.com/jaywink/federation/issues/82))
|
||||
|
||||
JSON encrypted payload encryption and decryption is handled by the Diaspora `EncryptedPayload` class.
|
||||
|
||||
* Add RFC3033 webfinger generator ([related issue](https://github.com/jaywink/federation/issues/108))
|
||||
|
||||
Also provided is a Django view and url configuration for easy addition into Django projects. Django is not a hard dependency of this library, usage of the Django view obviously requires installing Django itself. For configuration details see documentation.
|
||||
|
||||
### Changed
|
||||
|
||||
|
|
|
@ -19,3 +19,7 @@ recommonmark
|
|||
# Some datetime magic
|
||||
arrow
|
||||
freezegun
|
||||
|
||||
# Django support
|
||||
django>=1.8,<2.1
|
||||
pytest-django
|
||||
|
|
|
@ -58,9 +58,9 @@ Generator classes
|
|||
.. autoclass:: federation.hostmeta.generators.DiasporaWebFinger
|
||||
.. autoclass:: federation.hostmeta.generators.DiasporaHCard
|
||||
.. autoclass:: federation.hostmeta.generators.NodeInfo
|
||||
.. autoclass:: federation.hostmeta.generators.RFC3033Webfinger
|
||||
.. autoclass:: federation.hostmeta.generators.SocialRelayWellKnown
|
||||
|
||||
|
||||
Fetchers
|
||||
--------
|
||||
|
||||
|
@ -87,6 +87,35 @@ High level utility functions to pass outbound entities to. These should be favou
|
|||
.. autofunction:: federation.outbound.handle_create_payload
|
||||
.. autofunction:: federation.outbound.handle_send
|
||||
|
||||
Django
|
||||
------
|
||||
|
||||
Some ready provided views and URL configuration exist for Django.
|
||||
|
||||
Note! Django is not part of the normal requirements for this library. It must be installed separately.
|
||||
|
||||
.. autofunction:: federation.hostmeta.django.generators.rfc3033_webfinger_view
|
||||
|
||||
Configuration
|
||||
.............
|
||||
|
||||
To use the Django views, ensure a modern version of Django is installed and add the views to your URL config for example as follows. The URL's must be mounted on root if Diaspora protocol support is required.
|
||||
|
||||
::
|
||||
|
||||
url(r"", include("federation.hostmeta.django.urls")),
|
||||
|
||||
Some settings need to be set in Django settings. An example is below:
|
||||
|
||||
::
|
||||
|
||||
FEDERATION = {
|
||||
"base_url": "https://myserver.domain.tld,
|
||||
"profile_id_function": "myproject.utils.get_profile_id_by_handle",
|
||||
}
|
||||
|
||||
* ``base_url`` is the base URL of the server, ie protocol://domain.tld.
|
||||
* ``profile_id_function`` should be the full path to a function that given a handle will return the Diaspora URI format profile ID.
|
||||
|
||||
Protocols
|
||||
---------
|
||||
|
@ -107,10 +136,12 @@ Diaspora
|
|||
........
|
||||
|
||||
.. autofunction:: federation.utils.diaspora.fetch_public_key
|
||||
.. autofunction:: federation.utils.diaspora.generate_diaspora_profile_id
|
||||
.. autofunction:: federation.utils.diaspora.get_fetch_content_endpoint
|
||||
.. autofunction:: federation.utils.diaspora.get_public_endpoint
|
||||
.. autofunction:: federation.utils.diaspora.get_private_endpoint
|
||||
.. autofunction:: federation.utils.diaspora.get_public_endpoint
|
||||
.. autofunction:: federation.utils.diaspora.parse_diaspora_uri
|
||||
.. autofunction:: federation.utils.diaspora.parse_profile_diaspora_id
|
||||
.. autofunction:: federation.utils.diaspora.parse_profile_from_hcard
|
||||
.. autofunction:: federation.utils.diaspora.retrieve_and_parse_content
|
||||
.. autofunction:: federation.utils.diaspora.retrieve_and_parse_profile
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
from .generators import rfc3033_webfinger_view
|
|
@ -0,0 +1,71 @@
|
|||
import importlib
|
||||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.http import HttpResponseBadRequest, JsonResponse, HttpResponseNotFound
|
||||
|
||||
from federation.hostmeta.generators import RFC3033Webfinger
|
||||
|
||||
logger = logging.getLogger("federation")
|
||||
|
||||
|
||||
def get_configuration():
|
||||
"""
|
||||
Combine defaults with the Django configuration.
|
||||
"""
|
||||
configuration = {
|
||||
"hcard_path": "/hcard/users/",
|
||||
}
|
||||
configuration.update(settings.FEDERATION)
|
||||
if not all([
|
||||
"profile_id_function" in configuration,
|
||||
"base_url" in configuration,
|
||||
"hcard_path" in configuration,
|
||||
]):
|
||||
raise ImproperlyConfigured("Missing required FEDERATION settings, please check documentation.")
|
||||
return configuration
|
||||
|
||||
|
||||
def get_profile_id_func():
|
||||
"""
|
||||
Import the function to get profile ID by handle.
|
||||
"""
|
||||
config = get_configuration()
|
||||
profile_func_path = config.get("profile_id_function")
|
||||
module_path, func_name = profile_func_path.rsplit(".", 1)
|
||||
module = importlib.import_module(module_path)
|
||||
profile_func = getattr(module, func_name)
|
||||
return profile_func
|
||||
|
||||
|
||||
def rfc3033_webfinger_view(request, *args, **kwargs):
|
||||
"""
|
||||
Django view to generate an RFC3033 webfinger.
|
||||
"""
|
||||
resource = request.GET.get("resource")
|
||||
if not resource:
|
||||
return HttpResponseBadRequest("No resource found")
|
||||
if not resource.startswith("acct:"):
|
||||
return HttpResponseBadRequest("Invalid resource")
|
||||
|
||||
handle = resource.replace("acct:", "")
|
||||
profile_id_func = get_profile_id_func()
|
||||
|
||||
try:
|
||||
profile_id = profile_id_func(handle)
|
||||
except Exception as exc:
|
||||
logger.warning("rfc3033_webfinger_view - Failed to get profile ID by handle %s: %s", handle, exc)
|
||||
return HttpResponseNotFound()
|
||||
|
||||
config = get_configuration()
|
||||
webfinger = RFC3033Webfinger(
|
||||
id=profile_id,
|
||||
base_url=config.get('base_url'),
|
||||
hcard_path=config.get('hcard_path'),
|
||||
)
|
||||
|
||||
return JsonResponse(
|
||||
webfinger.render(),
|
||||
content_type="application/jrd+json",
|
||||
)
|
|
@ -0,0 +1,7 @@
|
|||
from django.conf.urls import url
|
||||
|
||||
from federation.hostmeta.django import rfc3033_webfinger_view
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^.well-known/webfinger$', rfc3033_webfinger_view, name="rfc3033-webfinger"),
|
||||
]
|
|
@ -1,14 +1,15 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import warnings
|
||||
|
||||
from base64 import b64encode
|
||||
import json
|
||||
import os
|
||||
import warnings
|
||||
from base64 import b64encode
|
||||
from string import Template
|
||||
|
||||
from jsonschema import validate
|
||||
from jsonschema.exceptions import ValidationError
|
||||
from xrd import XRD, Link, Element
|
||||
|
||||
from federation.utils.diaspora import parse_profile_diaspora_id
|
||||
|
||||
|
||||
def generate_host_meta(template=None, *args, **kwargs):
|
||||
"""Generate a host-meta XRD document.
|
||||
|
@ -55,7 +56,7 @@ def generate_hcard(template=None, **kwargs):
|
|||
return hcard.render()
|
||||
|
||||
|
||||
class BaseHostMeta(object):
|
||||
class BaseHostMeta:
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.xrd = XRD()
|
||||
|
||||
|
@ -152,7 +153,7 @@ class DiasporaWebFinger(BaseLegacyWebFinger):
|
|||
))
|
||||
|
||||
|
||||
class DiasporaHCard(object):
|
||||
class DiasporaHCard:
|
||||
"""Diaspora hCard document.
|
||||
|
||||
Must receive the `required` attributes as keyword arguments to init.
|
||||
|
@ -178,7 +179,7 @@ class DiasporaHCard(object):
|
|||
return self.template.substitute(self.kwargs)
|
||||
|
||||
|
||||
class SocialRelayWellKnown(object):
|
||||
class SocialRelayWellKnown:
|
||||
"""A `.well-known/social-relay` document in JSON.
|
||||
|
||||
For apps wanting to announce their preferences towards relay applications.
|
||||
|
@ -209,7 +210,7 @@ class SocialRelayWellKnown(object):
|
|||
validate(self.doc, schema)
|
||||
|
||||
|
||||
class NodeInfo(object):
|
||||
class NodeInfo:
|
||||
"""Generate a NodeInfo document.
|
||||
|
||||
See spec: http://nodeinfo.diaspora.software
|
||||
|
@ -276,3 +277,37 @@ def get_nodeinfo_well_known_document(url, document_path=None):
|
|||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
class RFC3033Webfinger:
|
||||
"""
|
||||
RFC 3033 webfinger - see https://diaspora.github.io/diaspora_federation/discovery/webfinger.html
|
||||
|
||||
A Django view is also available, see the child ``django`` module for view and url configuration.
|
||||
|
||||
:param id: Diaspora ID in URI format
|
||||
:param base_url: The base URL of the server (protocol://domain.tld)
|
||||
:param hcard_path: (Optional) hCard path, defaults to ``/hcard/users/``.
|
||||
:returns: dict
|
||||
"""
|
||||
def __init__(self, id, base_url, hcard_path="/hcard/users/"):
|
||||
self.handle, self.guid = parse_profile_diaspora_id(id)
|
||||
self.base_url = base_url
|
||||
self.hcard_path = hcard_path
|
||||
|
||||
def render(self):
|
||||
return {
|
||||
"subject": "acct:%s" % self.handle,
|
||||
"links": [
|
||||
{
|
||||
"rel": "http://microformats.org/profile/hcard",
|
||||
"type": "text/html",
|
||||
"href": "%s%s%s" % (self.base_url, self.hcard_path, self.guid),
|
||||
},
|
||||
{
|
||||
"rel": "http://joindiaspora.com/seed_location",
|
||||
"type": "text/html",
|
||||
"href": self.base_url,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
SECRET_KEY = "foobar"
|
||||
|
||||
INSTALLED_APPS = tuple()
|
||||
|
||||
FEDERATION = {
|
||||
"base_url": "https://example.com",
|
||||
"profile_id_function": "federation.tests.hostmeta.django.utils.get_profile_id_by_handle",
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
import json
|
||||
from unittest.mock import patch, Mock
|
||||
|
||||
from django.test import RequestFactory
|
||||
|
||||
from federation.hostmeta.django import rfc3033_webfinger_view
|
||||
from federation.hostmeta.django.generators import get_profile_id_func
|
||||
|
||||
|
||||
def test_get_profile_id_func():
|
||||
func = get_profile_id_func()
|
||||
assert callable(func)
|
||||
|
||||
|
||||
class TestRFC3033WebfingerView:
|
||||
def test_no_resource_returns_bad_request(self):
|
||||
request = RequestFactory().get("/.well-known/webfinger")
|
||||
response = rfc3033_webfinger_view(request)
|
||||
assert response.status_code == 400
|
||||
|
||||
def test_invalid_resource_returns_bad_request(self):
|
||||
request = RequestFactory().get("/.well-known/webfinger?resource=foobar")
|
||||
response = rfc3033_webfinger_view(request)
|
||||
assert response.status_code == 400
|
||||
|
||||
@patch("federation.hostmeta.django.generators.get_profile_id_func")
|
||||
def test_unknown_handle_returns_not_found(self, mock_get_func):
|
||||
mock_get_func.return_value = Mock(side_effect=Exception)
|
||||
request = RequestFactory().get("/.well-known/webfinger?resource=acct:foobar@domain.tld")
|
||||
response = rfc3033_webfinger_view(request)
|
||||
assert response.status_code == 404
|
||||
|
||||
def test_rendered_webfinger_returned(self):
|
||||
request = RequestFactory().get("/.well-known/webfinger?resource=acct:foobar@example.com")
|
||||
response = rfc3033_webfinger_view(request)
|
||||
assert response.status_code == 200
|
||||
assert response['Content-Type'] == "application/jrd+json"
|
||||
assert json.loads(response.content.decode("utf-8")) == {
|
||||
"subject": "acct:foobar@example.com",
|
||||
"links": [
|
||||
{
|
||||
"rel": "http://microformats.org/profile/hcard",
|
||||
"type": "text/html",
|
||||
"href": "https://example.com/hcard/users/1234",
|
||||
},
|
||||
{
|
||||
"rel": "http://joindiaspora.com/seed_location",
|
||||
"type": "text/html",
|
||||
"href": "https://example.com",
|
||||
},
|
||||
],
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
from federation.utils.diaspora import generate_diaspora_profile_id
|
||||
|
||||
|
||||
def get_profile_id_by_handle(handle):
|
||||
return generate_diaspora_profile_id(handle, "1234")
|
|
@ -1,15 +1,15 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import json
|
||||
from jsonschema import validate, ValidationError
|
||||
import pytest
|
||||
|
||||
from federation.hostmeta.generators import generate_host_meta, generate_legacy_webfinger, generate_hcard, \
|
||||
SocialRelayWellKnown, NodeInfo, get_nodeinfo_well_known_document
|
||||
from federation.hostmeta.generators import (
|
||||
generate_host_meta, generate_legacy_webfinger, generate_hcard,
|
||||
SocialRelayWellKnown, NodeInfo, get_nodeinfo_well_known_document, RFC3033Webfinger,
|
||||
)
|
||||
from federation.tests.fixtures.payloads import DIASPORA_HOSTMETA, DIASPORA_WEBFINGER
|
||||
|
||||
|
||||
class TestDiasporaHostMetaGenerator(object):
|
||||
|
||||
class TestDiasporaHostMetaGenerator:
|
||||
def test_generate_valid_host_meta(self):
|
||||
hostmeta = generate_host_meta("diaspora", webfinger_host="https://example.com")
|
||||
assert hostmeta.decode("UTF-8") == DIASPORA_HOSTMETA
|
||||
|
@ -19,8 +19,7 @@ class TestDiasporaHostMetaGenerator(object):
|
|||
generate_host_meta("diaspora")
|
||||
|
||||
|
||||
class TestDiasporaWebFingerGenerator(object):
|
||||
|
||||
class TestDiasporaWebFingerGenerator:
|
||||
def test_generate_valid_webfinger(self):
|
||||
webfinger = generate_legacy_webfinger(
|
||||
"diaspora",
|
||||
|
@ -36,8 +35,7 @@ class TestDiasporaWebFingerGenerator(object):
|
|||
generate_legacy_webfinger("diaspora")
|
||||
|
||||
|
||||
class TestDiasporaHCardGenerator(object):
|
||||
|
||||
class TestDiasporaHCardGenerator:
|
||||
def test_generate_valid_hcard(self):
|
||||
with open("federation/hostmeta/templates/hcard_diaspora.html") as f:
|
||||
template = f.read().replace("$", "")
|
||||
|
@ -77,8 +75,7 @@ class TestDiasporaHCardGenerator(object):
|
|||
)
|
||||
|
||||
|
||||
class TestSocialRelayWellKnownGenerator(object):
|
||||
|
||||
class TestSocialRelayWellKnownGenerator:
|
||||
def test_valid_social_relay_well_known(self):
|
||||
with open("federation/hostmeta/schemas/social-relay-well-known.json") as f:
|
||||
schema = json.load(f)
|
||||
|
@ -125,7 +122,7 @@ class TestSocialRelayWellKnownGenerator(object):
|
|||
well_known.render()
|
||||
|
||||
|
||||
class TestNodeInfoGenerator(object):
|
||||
class TestNodeInfoGenerator:
|
||||
def _valid_nodeinfo(self, raise_on_validate=False):
|
||||
return NodeInfo(
|
||||
software={"name": "diaspora", "version": "0.5.4.3"},
|
||||
|
@ -179,3 +176,25 @@ class TestNodeInfoGenerator(object):
|
|||
wellknown = get_nodeinfo_well_known_document("https://example.com")
|
||||
assert wellknown["links"][0]["rel"] == "http://nodeinfo.diaspora.software/ns/schema/1.0"
|
||||
assert wellknown["links"][0]["href"] == "https://example.com/nodeinfo/1.0"
|
||||
|
||||
|
||||
def test_rfc3033_webfinger():
|
||||
webfinger = RFC3033Webfinger(
|
||||
"diaspora://foobar@example.com/profile/1234",
|
||||
"https://example.com",
|
||||
).render()
|
||||
assert webfinger == {
|
||||
"subject": "acct:foobar@example.com",
|
||||
"links": [
|
||||
{
|
||||
"rel": "http://microformats.org/profile/hcard",
|
||||
"type": "text/html",
|
||||
"href": "https://example.com/hcard/users/1234",
|
||||
},
|
||||
{
|
||||
"rel": "http://joindiaspora.com/seed_location",
|
||||
"type": "text/html",
|
||||
"href": "https://example.com",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
|
|
@ -11,7 +11,8 @@ from federation.utils.diaspora import (
|
|||
retrieve_diaspora_hcard, 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, parse_diaspora_uri,
|
||||
retrieve_and_parse_diaspora_webfinger, parse_diaspora_webfinger, get_public_endpoint, get_private_endpoint)
|
||||
retrieve_and_parse_diaspora_webfinger, parse_diaspora_webfinger, get_public_endpoint, get_private_endpoint,
|
||||
parse_profile_diaspora_id, generate_diaspora_profile_id)
|
||||
|
||||
|
||||
class TestParseDiasporaWebfinger:
|
||||
|
@ -48,6 +49,16 @@ def test_parse_diaspora_uri():
|
|||
assert not parse_diaspora_uri("spam and eggs")
|
||||
|
||||
|
||||
def test_parse_profile_diaspora_id():
|
||||
assert parse_profile_diaspora_id("diaspora://foobar@example.com/profile/1234") == ("foobar@example.com", "1234")
|
||||
with pytest.raises(ValueError):
|
||||
assert parse_profile_diaspora_id("diaspora://foobar@example.com/like/1234")
|
||||
|
||||
|
||||
def test_generate_diaspora_profile_id():
|
||||
assert generate_diaspora_profile_id("foobar@example.com", "1234") == "diaspora://foobar@example.com/profile/1234"
|
||||
|
||||
|
||||
class TestRetrieveDiasporaHCard:
|
||||
@patch("federation.utils.diaspora.retrieve_and_parse_diaspora_webfinger", return_value={
|
||||
"hcard_url": "http://localhost",
|
||||
|
|
|
@ -151,6 +151,25 @@ def parse_diaspora_uri(uri):
|
|||
return handle, entity_type, guid
|
||||
|
||||
|
||||
def parse_profile_diaspora_id(id):
|
||||
"""
|
||||
Parse profile handle and guid from diaspora ID.
|
||||
"""
|
||||
handle, entity_type, guid = parse_diaspora_uri(id)
|
||||
if entity_type != "profile":
|
||||
raise ValueError(
|
||||
"Invalid entity type %s to generate private remote endpoint for delivery. Must be 'profile'." % entity_type
|
||||
)
|
||||
return handle, guid
|
||||
|
||||
|
||||
def generate_diaspora_profile_id(handle, guid):
|
||||
"""
|
||||
Generate a Diaspora profile ID from handle and guid.
|
||||
"""
|
||||
return "diaspora://%s/profile/%s" % (handle, guid)
|
||||
|
||||
|
||||
def parse_profile_from_hcard(hcard, handle):
|
||||
"""
|
||||
Parse all the fields we can from a hCard document to get a Profile.
|
||||
|
@ -244,10 +263,6 @@ def get_public_endpoint(id):
|
|||
|
||||
def get_private_endpoint(id):
|
||||
"""Get remote endpoint for delivering private payloads."""
|
||||
handle, entity_type, guid = parse_diaspora_uri(id)
|
||||
if entity_type != "profile":
|
||||
raise ValueError(
|
||||
"Invalid entity type %s to generate private remote endpoint for delivery. Must be 'profile'." % entity_type
|
||||
)
|
||||
handle, guid = parse_profile_diaspora_id(id)
|
||||
_username, domain = handle.split("@")
|
||||
return "https://%s/receive/users/%s" % (domain, guid)
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
[pytest]
|
||||
testpaths = federation
|
||||
DJANGO_SETTINGS_MODULE = federation.tests.hostmeta.django.settings
|
||||
|
|
Ładowanie…
Reference in New Issue